//! Interactive window drags on `Windows`: edge-handle resize and title-bar
//! move of floating windows (Wayland and Xwayland), snap zones, committing the
//! live rect and persisting the final geometry.

use super::*;

impl Windows {
    /// Work area of the screen under `pos` for drag purposes: the primary's
    /// available area, or an extend head's full region (no chrome there).
    pub(super) fn drag_area_at(&self, pos: Point<f64, Logical>) -> Rectangle<i32, Logical> {
        let target = self.output_at(pos);
        if target.smithay_output().name() == self.output.smithay_output().name() {
            self.output.available()
        } else {
            target.logical_rect()
        }
    }

    pub fn is_resize_dragging(&self) -> bool {
        self.resize_drag.is_some() || self.resize_drag_x11.is_some()
    }

    /// Look for a resize grab zone under `pointer` on any visible floating
    /// window in the active layout; start a resize drag if found.
    ///
    /// Uses the same topmost-first-with-body-occlusion walk as
    /// [`resize_cursor_edge`] so a resize drag cannot start on a window whose
    /// grab band is covered by another window stacked on top of it.
    pub fn start_resize_drag(&mut self, pointer: Point<f64, Logical>) -> bool {
        if self.canvas.is_mobile() {
            return false;
        }

        if let Some((wl, edge, rect)) = self.x11_windows.resize_hit_test(pointer, &self.toplevel_z_order) {
            if !self.x11_hit_occluded(&wl, pointer) {
                self.set_focus(Some(FocusTarget::X11(wl.clone())));
                self.resize_drag_x11 = Some(ResizeDragX11 {
                    wl, edge, start_pointer: pointer, start_rect: rect,
                });
                self.dirty = true;
                return true;
            }
        }

        let scale = self.canvas.scale();
        let bar_h = crate::ssd::SsdDecoration::bar_height();

        let mut hit: Option<(Rc<RefCell<Window>>, crate::ssd::ResizeEdge, Rectangle<i32, Logical>)> = None;
        'outer: for layout in self.layouts.stacking_layouts() {
            for window_rc in layout.windows() {
                let (rect, eligible, minimized, bar) = {
                    let w = window_rc.borrow();
                    let eligible = w.floating && !w.minimized && !w.is_maximized();
                    let bar = if w.suppress_ssd(&self.csd_apps) { 0 } else { bar_h };
                    (w.bounds(scale), eligible, w.minimized, bar)
                };
                if minimized { continue; }
                if crate::ssd::body_hit_test(rect, pointer, bar) {
                    break 'outer;
                }
                if !eligible { continue; }
                if let Some(edge) = crate::ssd::resize_hit_test(rect, pointer, bar) {
                    let surface = window_rc.borrow().surface.surface().clone();
                    if self.layout_hit_occluded_by_x11(self.z_index(&surface), pointer) {
                        break 'outer;
                    }
                    hit = Some((window_rc.clone(), edge, rect));
                    break 'outer;
                }
            }
        }

        if let Some((window_rc, edge, rect)) = hit {
            self.resize_drag = Some(ResizeDrag {
                window: Rc::downgrade(&window_rc),
                edge,
                start_pointer: pointer,
                start_rect: rect,
            });
            self.set_focus(Some(FocusTarget::Layout(Rc::downgrade(&window_rc))));
            window_rc.borrow_mut().set_resizing(true);
            self.dirty = true;
            return true;
        }
        false
    }

    pub fn update_resize_drag(&mut self, pointer: Point<f64, Logical>) {
        if let Some(drag) = &self.resize_drag_x11 {
            use crate::ssd::ResizeEdge;
            let dx = (pointer.x - drag.start_pointer.x).round() as i32;
            let dy = (pointer.y - drag.start_pointer.y).round() as i32;
            let start = drag.start_rect;
            let (mut x, mut y, mut w, mut h) = (start.loc.x, start.loc.y, start.size.w, start.size.h);
            match drag.edge {
                ResizeEdge::E  => { w = start.size.w + dx; }
                ResizeEdge::W  => { x = start.loc.x + dx; w = start.size.w - dx; }
                ResizeEdge::S  => { h = start.size.h + dy; }
                ResizeEdge::N  => { y = start.loc.y + dy; h = start.size.h - dy; }
                ResizeEdge::SE => { w = start.size.w + dx; h = start.size.h + dy; }
                ResizeEdge::NE => { w = start.size.w + dx; y = start.loc.y + dy; h = start.size.h - dy; }
                ResizeEdge::SW => { x = start.loc.x + dx; w = start.size.w - dx; h = start.size.h + dy; }
                ResizeEdge::NW => { x = start.loc.x + dx; w = start.size.w - dx; y = start.loc.y + dy; h = start.size.h - dy; }
            }
            let (min_w, min_h) = (100, 80);
            if w < min_w {
                if matches!(drag.edge, ResizeEdge::W | ResizeEdge::SW | ResizeEdge::NW) {
                    x = start.loc.x + start.size.w - min_w;
                }
                w = min_w;
            }
            if h < min_h {
                if matches!(drag.edge, ResizeEdge::N | ResizeEdge::NE | ResizeEdge::NW) {
                    y = start.loc.y + start.size.h - min_h;
                }
                h = min_h;
            }
            let bar_h = crate::ssd::SsdDecoration::bar_height();
            if y < bar_h { y = bar_h; }
            let rect = Rectangle::<i32, Logical>::new((x, y).into(), (w, h).into());
            if let Some(s) = self.x11_windows.find(&drag.wl) {
                let _ = s.configure(rect);
            }
            self.dirty = true;
            return;
        }

        let Some(drag) = &self.resize_drag else { return };
        let Some(window_rc) = drag.window.upgrade() else {
            self.resize_drag = None;
            return;
        };

        use crate::ssd::ResizeEdge;
        let dx = (pointer.x - drag.start_pointer.x).round() as i32;
        let dy = (pointer.y - drag.start_pointer.y).round() as i32;

        let start = drag.start_rect;
        let (mut x, mut y, mut w, mut h) =
            (start.loc.x, start.loc.y, start.size.w, start.size.h);

        match drag.edge {
            ResizeEdge::E                          => { w = start.size.w + dx; }
            ResizeEdge::W                          => { x = start.loc.x + dx; w = start.size.w - dx; }
            ResizeEdge::S                          => { h = start.size.h + dy; }
            ResizeEdge::N                          => { y = start.loc.y + dy; h = start.size.h - dy; }
            ResizeEdge::SE                         => { w = start.size.w + dx; h = start.size.h + dy; }
            ResizeEdge::NE                         => { w = start.size.w + dx; y = start.loc.y + dy; h = start.size.h - dy; }
            ResizeEdge::SW                         => { x = start.loc.x + dx; w = start.size.w - dx; h = start.size.h + dy; }
            ResizeEdge::NW                         => { x = start.loc.x + dx; w = start.size.w - dx; y = start.loc.y + dy; h = start.size.h - dy; }
        }

        let (min_size, max_size) = window_rc.borrow().size_constraints();
        // Toolkits that set their own minimum (Firefox: ~450×300, GTK: 360×220,
        // electron: ~320×240) used to clamp at our local minimum first because
        // we max'd against (100, 80). The .max(client) below now respects the
        // larger of the two: if the client reported a sane minimum we use
        // that; if it didn't (min_size.{w,h} == 0), we fall back to (100, 80)
        // so an absent set_min_size never leaves the window with a single-
        // pixel rect. Same idea for max — clients that set a max get clamped
        // accordingly; everything else is unbounded.
        let min_w = min_size.w.max(100);
        let min_h = min_size.h.max(80);
        let max_w = if max_size.w > 0 { max_size.w } else { i32::MAX };
        let max_h = if max_size.h > 0 { max_size.h } else { i32::MAX };

        // Clamp size, and if a west/north edge is shrinking past the min,
        // pin the opposite edge by adjusting loc.
        if w < min_w {
            if matches!(drag.edge, ResizeEdge::W | ResizeEdge::SW | ResizeEdge::NW) {
                x = start.loc.x + start.size.w - min_w;
            }
            w = min_w;
        }
        if h < min_h {
            if matches!(drag.edge, ResizeEdge::N | ResizeEdge::NE | ResizeEdge::NW) {
                y = start.loc.y + start.size.h - min_h;
            }
            h = min_h;
        }
        w = w.min(max_w);
        h = h.min(max_h);

        // Stop the N / NE / NW edges from pulling the window up under the
        // topbar — the topbar eats pointer events in that strip, so a
        // window whose top edge crossed into it could no longer be moved
        // or resized. Clamp `y` to the top of the available area (which
        // already accounts for the topbar height + any layer-shell
        // exclusive zones) and keep the SE corner of the window pinned by
        // shrinking `h` to compensate.
        if matches!(drag.edge, ResizeEdge::N | ResizeEdge::NE | ResizeEdge::NW) {
            let avail = self.owner_area(window_rc.borrow().output_rectangle());
            if y < avail.loc.y {
                let delta = avail.loc.y - y;
                y = avail.loc.y;
                h = (h - delta).max(min_h);
            }
        }

        let rect = Rectangle::<i32, Logical>::new((x, y).into(), (w, h).into());

        // Trace a single line per first-of-kind edge so a stuck resize is
        // diagnosable from the journal without flooding the log on every
        // pointer motion. Firefox not resizing is the canonical bug here:
        // the configure goes out, the client acks a size that doesn't match
        // (toolkit-rounded, fractional-scale-snapped, etc) and the
        // transaction never completes. The log captures requested vs acked
        // so the divergence is visible.
        {
            let w_borrow = window_rc.borrow();
            let app_id = w_borrow.app_id.as_deref().unwrap_or("?");
            let acked  = w_borrow.acked_size;
            tracing::debug!(
                "resize {app_id}: edge={:?} req=({}x{}) acked=({}x{}) min=({}x{}) max=({}x{})",
                drag.edge, w, h, acked.w, acked.h,
                min_size.w, min_size.h, max_size.w, max_size.h,
            );
        }

        window_rc.borrow_mut().set_dimensions_interactive(self.output.scale(), rect);
        self.dirty = true;
    }

    pub fn end_resize_drag(&mut self) {
        if self.resize_drag_x11.take().is_some() {
            self.dirty = true;
            return;
        }
        let Some(drag) = self.resize_drag.take() else { return };
        if let Some(window_rc) = drag.window.upgrade() {
            window_rc.borrow_mut().set_resizing(false);
            // The rect was committed live during update_resize_drag via
            // set_dimensions_interactive — by the time end_resize_drag
            // runs the window already holds the user's final size. Save
            // it so the next launch of the same app picks it up.
            self.record_geometry(&mut window_rc.borrow_mut());
        }
    }
}

// Interactive move drag: title-bar drag to reposition a floating window,
// with edge snap zones (halves and quarters) on modifier-click drags.
impl Windows {

    /// True while a title-bar drag is in progress.
    pub fn is_move_dragging(&self) -> bool {
        self.move_drag.is_some() || self.move_drag_x11.is_some()
    }

    /// Start a title-bar drag on the window under `pointer`, if any.
    /// Returns `true` if a drag was started. Only applies to floating
    /// windows in desktop mode.
    pub fn start_move_drag(&mut self, pointer: Point<f64, Logical>) -> bool {
        if self.canvas.is_mobile() {
            return false;
        }

        if let Some((wl, action)) = self.x11_windows.ssd_hit_test(pointer, &self.toplevel_z_order) {
            if !self.x11_hit_occluded(&wl, pointer) {
                if action != crate::ssd::SsdAction::Move { return false; }
                let Some(start_loc) = self.x11_windows.find(&wl).map(|s| s.geometry().loc) else {
                    return false;
                };
                self.set_focus(Some(FocusTarget::X11(wl.clone())));
                self.move_drag_x11 = Some(MoveDragX11 {
                    wl, start_pointer: pointer, start_loc,
                    snap: None,
                    snap_avail: self.output.available(),
                    snap_enabled: false,
                });
                self.dirty = true;
                return true;
            }
        }

        let Some(( window_rc, action )) = self.ssd_window_hit_test( pointer ) else {
            return false;
        };
        if action != crate::ssd::SsdAction::Move {
            return false;
        }

        // Always raise the clicked window to the front and focus it — both
        // floating and tiled. Tiled windows skip the actual drag (only
        // floating windows reposition), but the press still brings them to
        // the front, moves keyboard focus and counts as an SSD interaction
        // for the caller's double-click detection.
        self.set_focus(Some(FocusTarget::Layout(Rc::downgrade(&window_rc))));

        let start_loc = {
            let mut w = window_rc.borrow_mut();
            if !w.floating {
                self.dirty = true;
                return true;
            }
            // Windows-style tear-off: dragging a maximized window restores
            // its previous size and repositions it so the title bar stays
            // under the pointer.
            if w.is_maximized() {
                let old_rect = w.output_rectangle();
                w.unmaximize(self.output.scale());
                let new_size = w.output_rectangle().size;
                let ratio = if old_rect.size.w > 0 {
                    ((pointer.x as i32) - old_rect.loc.x) as f64 / old_rect.size.w as f64
                } else {
                    0.5
                };
                let new_x = (pointer.x as i32) - (ratio * new_size.w as f64) as i32;
                let bar_h = crate::ssd::SsdDecoration::bar_height();
                let new_y = (pointer.y as i32) - bar_h / 2;
                w.set_floating_position((new_x, new_y).into());
            }
            w.output_rectangle().loc
        };

        self.move_drag = Some(MoveDrag {
            window: Rc::downgrade(&window_rc),
            start_pointer: pointer,
            start_loc,
            snap: None,
            snap_avail: self.output.available(),
            snap_enabled: false,
        });

        self.dirty = true;
        true
    }


    /// Start a move drag from a press anywhere on a window's body — the
    /// modifier-click move, for when the bar is off-screen or covered.
    /// Resolves the topmost window under `pointer` through the regular
    /// hit test, so stacking and occlusion match what the user sees.
    /// Only floating, unmaximized windows drag; everything else returns
    /// `false` so the click reaches the client untouched.
    pub fn start_move_drag_anywhere(&mut self, pointer: Point<f64, Logical>) -> bool {
        use crate::windows::surface::InputSurfaceKind;
        if self.canvas.is_mobile() {
            return false;
        }
        match self.surface_at(pointer).and_then(|s| s.toplevel) {
            Some(InputSurfaceKind::Layout((weak, _))) => {
                let Some(window_rc) = weak.upgrade() else { return false };
                let start_loc = {
                    let w = window_rc.borrow();
                    if !w.floating || w.is_maximized() {
                        return false;
                    }
                    w.output_rectangle().loc
                };
                self.set_focus(Some(FocusTarget::Layout(Rc::downgrade(&window_rc))));
                self.move_drag = Some(MoveDrag {
                    window: Rc::downgrade(&window_rc),
                    start_pointer: pointer,
                    start_loc,
                    snap: None,
                    snap_avail: self.output.available(),
                    snap_enabled: true,
                });
                self.dirty = true;
                true
            }
            Some(InputSurfaceKind::X11(wl)) => {
                let Some(start_loc) = self.x11_windows.find(&wl).map(|s| s.geometry().loc) else {
                    return false;
                };
                self.z_promote(&wl);
                self.set_focus(Some(FocusTarget::X11(wl.clone())));
                self.move_drag_x11 = Some(MoveDragX11 {
                    wl, start_pointer: pointer, start_loc,
                    snap: None,
                    snap_avail: self.output.available(),
                    snap_enabled: true,
                });
                self.dirty = true;
                true
            }
            _ => false,
        }
    }

    pub fn start_move_drag_for(
        &mut self,
        surface: &ToplevelSurface,
        pointer: Point<f64, Logical>,
    ) -> bool {
        if self.canvas.is_mobile() {
            return false;
        }

        let Some(window_rc) = self.find_toplevel(surface) else { return false };

        let start_loc = {
            let mut w = window_rc.borrow_mut();
            if !w.floating {
                return false;
            }
            if w.is_maximized() {
                let old_rect = w.output_rectangle();
                w.unmaximize(self.output.scale());
                let new_size = w.output_rectangle().size;
                let ratio = if old_rect.size.w > 0 {
                    ((pointer.x as i32) - old_rect.loc.x) as f64 / old_rect.size.w as f64
                } else {
                    0.5
                };
                let new_x = (pointer.x as i32) - (ratio * new_size.w as f64) as i32;
                let new_y = (pointer.y as i32) - 16;
                w.set_floating_position((new_x, new_y).into());
            }
            w.output_rectangle().loc
        };

        self.move_drag = Some(MoveDrag {
            window: Rc::downgrade(&window_rc),
            start_pointer: pointer,
            start_loc,
            snap: None,
            snap_avail: self.output.available(),
            snap_enabled: false,
        });

        self.set_focus(Some(FocusTarget::Layout(Rc::downgrade(&window_rc))));
        self.dirty = true;
        true
    }

    /// Start an interactive resize on `surface` with the given `edge`, anchored
    /// at the current `pointer` position. Mirrors `start_move_drag_for` for the
    /// `xdg_toplevel.resize` request issued by CSD clients (Brave/Electron drag
    /// from a corner of their own header bar). The hit-test based
    /// `start_resize_drag` keeps serving SSD pointer presses on the outer edge
    /// band; this surface-driven variant exists so CSD clients can arm a resize
    /// without ever entering the SSD strip.
    pub fn start_resize_drag_for(
        &mut self,
        surface: &ToplevelSurface,
        edge: crate::ssd::ResizeEdge,
        pointer: Point<f64, Logical>,
    ) -> bool {
        if self.canvas.is_mobile() {
            return false;
        }

        let Some(window_rc) = self.find_toplevel(surface) else { return false };

        let scale = self.canvas.scale();
        let start_rect = {
            let w = window_rc.borrow();
            if !w.floating || w.minimized || w.is_maximized() {
                return false;
            }
            w.bounds(scale)
        };

        self.resize_drag = Some(ResizeDrag {
            window: Rc::downgrade(&window_rc),
            edge,
            start_pointer: pointer,
            start_rect,
        });

        self.set_focus(Some(FocusTarget::Layout(Rc::downgrade(&window_rc))));
        window_rc.borrow_mut().set_resizing(true);
        self.dirty = true;
        true
    }


    /// X11 counterparts of the surface-driven starters above, for CSD X11
    /// clients arming a drag via `_NET_WM_MOVERESIZE`.
    pub fn start_move_drag_for_x11(
        &mut self,
        surface: &smithay::xwayland::X11Surface,
        pointer: Point<f64, Logical>,
    ) -> bool {
        if self.canvas.is_mobile() {
            return false;
        }
        let Some(wl) = surface.wl_surface() else { return false };
        self.z_promote(&wl);
        self.set_focus(Some(FocusTarget::X11(wl.clone())));
        self.move_drag_x11 = Some(MoveDragX11 {
            wl,
            start_pointer: pointer,
            start_loc: surface.geometry().loc,
            snap: None,
            snap_avail: self.output.available(),
            snap_enabled: false,
        });
        self.dirty = true;
        true
    }

    pub fn start_resize_drag_for_x11(
        &mut self,
        surface: &smithay::xwayland::X11Surface,
        edge: crate::ssd::ResizeEdge,
        pointer: Point<f64, Logical>,
    ) -> bool {
        if self.canvas.is_mobile() {
            return false;
        }
        let Some(wl) = surface.wl_surface() else { return false };
        self.z_promote(&wl);
        self.set_focus(Some(FocusTarget::X11(wl.clone())));
        self.resize_drag_x11 = Some(ResizeDragX11 {
            wl,
            edge,
            start_pointer: pointer,
            start_rect: surface.geometry(),
        });
        self.dirty = true;
        true
    }

    /// Update the dragged window's position from the current pointer.
    pub fn update_move_drag(&mut self, pointer: Point<f64, Logical>) {
        let frac = self.snap_edge_fraction;
        if self.move_drag_x11.is_some() {
            let avail = self.drag_area_at(pointer);
            let drag = self.move_drag_x11.as_mut().unwrap();
            drag.snap = if drag.snap_enabled { SnapZone::detect(pointer, avail, frac) } else { None };
            drag.snap_avail = avail;
            let dx = (pointer.x - drag.start_pointer.x).round() as i32;
            let dy = (pointer.y - drag.start_pointer.y).round() as i32;
            let mut loc = drag.start_loc;
            loc.x += dx;
            loc.y += dy;
            let wl = drag.wl.clone();
            if let Some(s) = self.x11_windows.find(&wl) {
                let mut geo = s.geometry();
                geo.loc = loc;
                let _ = s.configure(geo);
            }
            self.dirty = true;
            return;
        }

        // The drag works against the screen under the cursor: snap zones,
        // the title-bar reachability clamp, and the eventual commit all
        // follow the pointer's screen, so a window dragged onto an extend
        // head stays there instead of snapping back to the primary.
        let avail = self.drag_area_at(pointer);

        let Some(drag) = &mut self.move_drag else { return };
        let new_snap = if drag.snap_enabled { SnapZone::detect(pointer, avail, frac) } else { None };
        let Some(window_rc) = drag.window.upgrade() else {
            self.move_drag = None;
            return;
        };

        // Track the snap target on the drag state so the textures pass
        // can paint the live preview overlay and `end_move_drag` knows
        // what to commit. Repaint when the zone changes (preview moves).
        let snap_changed = drag.snap != new_snap;
        drag.snap = new_snap;
        drag.snap_avail = avail;

        let dx = (pointer.x - drag.start_pointer.x).round() as i32;
        let dy = (pointer.y - drag.start_pointer.y).round() as i32;
        let mut loc = drag.start_loc;
        loc.x += dx;
        loc.y += dy;

        // Clamp so the title bar stays at least partially reachable.
        let bar_h = self.bar_h_for(&window_rc.borrow());
        let min_y = avail.loc.y + bar_h; // window top can go down to 0 from bar's POV
        let max_y = avail.loc.y + avail.size.h - 16; // keep a sliver visible
        let min_x = avail.loc.x - 200; // allow partial offscreen
        let max_x = avail.loc.x + avail.size.w - 64;
        loc.x = loc.x.clamp(min_x, max_x);
        loc.y = loc.y.clamp(min_y, max_y);

        window_rc.borrow_mut().set_floating_position(loc);
        self.dirty |= snap_changed;
        self.dirty = true;
    }

    /// End a title-bar drag. When a modifier-click drag was hovering a
    /// snap zone, commit it as a half / quarter resize; otherwise just
    /// leave the window where the drag last placed it.
    pub fn end_move_drag(&mut self) {
        if let Some(drag) = self.move_drag_x11.take() {
            if let Some(zone) = drag.snap {
                let target = zone.rect(drag.snap_avail);
                if let Some(s) = self.x11_windows.find(&drag.wl) {
                    let _ = s.configure(target);
                }
            }
            self.dirty = true;
            return;
        }
        let Some(drag) = self.move_drag.take() else { return };
        let Some(window_rc) = drag.window.upgrade() else { return };

        // Commit a snap-zone target before persisting, so the saved
        // rect reflects the user's final intent (drag to half/quarter),
        // not the floating rect mid-drag.
        if let Some(zone) = drag.snap {
            let target = zone.rect(drag.snap_avail);
            let scale  = self.output.scale();
            window_rc.borrow_mut().set_dimensions(scale, target);
        }
        // Persist for next-launch restore (eydos #7). Even a no-snap
        // drag updates the saved rect: the user moved the window, we
        // remember where they put it. `record_geometry` is a no-op
        // when app_id is empty, so the very first commit (before the
        // client has set its app_id) doesn't store an unkeyable rect.
        self.record_geometry(&mut window_rc.borrow_mut());
        self.refresh_output_overlaps();
        self.dirty = true;
    }

    /// Snap zone hovered by the in-flight move drag (Wayland or X11),
    /// with the work area it was detected against. Feeds the preview
    /// overlay in the textures passes.
    pub(super) fn active_snap(&self) -> Option<(SnapZone, Rectangle<i32, Logical>)> {
        if let Some(d) = &self.move_drag {
            if let Some(z) = d.snap { return Some((z, d.snap_avail)); }
        }
        if let Some(d) = &self.move_drag_x11 {
            if let Some(z) = d.snap { return Some((z, d.snap_avail)); }
        }
        None
    }
}

// Drag state carried on `Windows` (fields live in mod.rs).
#[derive(Debug)]
pub struct MoveDragX11 {
    pub wl: WlSurface,
    pub start_pointer: Point<f64, Logical>,
    pub start_loc: Point<i32, Logical>,
    pub snap: Option<SnapZone>,
    pub snap_avail: Rectangle<i32, Logical>,
    /// Snap zones only arm on modifier-click drags; plain title-bar
    /// drags just move the window.
    pub snap_enabled: bool,
}

#[derive(Debug)]
pub struct MoveDrag {
    pub window: Weak<RefCell<Window>>,
    pub start_pointer: Point<f64, Logical>,
    pub start_loc: Point<i32, Logical>,
    /// Snap zone the cursor is currently hovering, if any. `None` means
    /// "release here drops the window in place"; `Some(zone)` means
    /// "release here resizes / moves the window to the zone's rect".
    /// Updated on every `update_move_drag` and consumed by
    /// `end_move_drag`.
    pub snap: Option<SnapZone>,
    /// Work area the current `snap` was detected against — the screen under
    /// the cursor. The commit on release must target the same area, or a
    /// drag onto an extend head would snap back to the primary.
    pub snap_avail: Rectangle<i32, Logical>,
    /// Snap zones only arm on modifier-click drags; plain title-bar
    /// drags just move the window.
    pub snap_enabled: bool,
}

/// Edge-snap target detected from the live drag cursor. Only armed on
/// modifier-click drags (Ctrl+Alt / Super + left press). The pointer
/// must be near a screen edge; its position along that edge, split in
/// thirds, picks a corner quarter or the edge's half.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SnapZone {
    Left,
    Right,
    Top,
    Bottom,
    NW,
    NE,
    SW,
    SE,
}

impl SnapZone {
    /// Detect the snap zone matching `pointer` against the available
    /// output rect, or return `None` when the cursor is away from every
    /// edge. `edge_fraction` is the band depth as a fraction of the work
    /// area's width (side bands) or height (top / bottom bands). Inside a
    /// band, the outer thirds of that edge map to the adjacent corner
    /// quarters and the middle third to the edge's half.
    pub fn detect(
        pointer: Point<f64, Logical>,
        avail:   Rectangle<i32, Logical>,
        edge_fraction: f64,
    ) -> Option<Self> {
        let l = avail.loc.x as f64;
        let t = avail.loc.y as f64;
        let r = (avail.loc.x + avail.size.w) as f64;
        let b = (avail.loc.y + avail.size.h) as f64;
        let x = pointer.x;
        let y = pointer.y;

        let band_w = avail.size.w as f64 * edge_fraction;
        let band_h = avail.size.h as f64 * edge_fraction;
        let near_l = x - l < band_w;
        let near_r = r - x < band_w;
        let near_t = y - t < band_h;
        let near_b = b - y < band_h;

        let third = |pos: f64, start: f64, len: f64| -> u8 {
            let rel = (pos - start) / len;
            if rel < 1.0 / 3.0 { 0 } else if rel < 2.0 / 3.0 { 1 } else { 2 }
        };
        let col = third(x, l, avail.size.w as f64);
        let row = third(y, t, avail.size.h as f64);

        if near_t {
            return Some(match col { 0 => SnapZone::NW, 1 => SnapZone::Top, _ => SnapZone::NE });
        }
        if near_b {
            return Some(match col { 0 => SnapZone::SW, 1 => SnapZone::Bottom, _ => SnapZone::SE });
        }
        if near_l {
            return Some(match row { 0 => SnapZone::NW, 1 => SnapZone::Left, _ => SnapZone::SW });
        }
        if near_r {
            return Some(match row { 0 => SnapZone::NE, 1 => SnapZone::Right, _ => SnapZone::SE });
        }
        None
    }

    /// Concrete rect this zone snaps a window to, given the available
    /// output area (which is already topbar-offset by `Output::available`).
    pub fn rect(self, avail: Rectangle<i32, Logical>) -> Rectangle<i32, Logical> {
        let l = avail.loc.x;
        let t = avail.loc.y;
        let w = avail.size.w;
        let h = avail.size.h;
        let half_w = w / 2;
        let half_h = h / 2;
        match self {
            SnapZone::Left   => Rectangle::new((l, t).into(),                 (half_w, h).into()),
            SnapZone::Right  => Rectangle::new((l + half_w, t).into(),        (w - half_w, h).into()),
            SnapZone::Top    => Rectangle::new((l, t).into(),                 (w, half_h).into()),
            SnapZone::Bottom => Rectangle::new((l, t + half_h).into(),        (w, h - half_h).into()),
            SnapZone::NW     => Rectangle::new((l, t).into(),                 (half_w, half_h).into()),
            SnapZone::NE     => Rectangle::new((l + half_w, t).into(),        (w - half_w, half_h).into()),
            SnapZone::SW     => Rectangle::new((l, t + half_h).into(),        (half_w, h - half_h).into()),
            SnapZone::SE     => Rectangle::new((l + half_w, t + half_h).into(), (w - half_w, h - half_h).into()),
        }
    }
}

/// State for an in-progress edge-handle resize drag.
#[derive(Debug)]
pub struct ResizeDrag {
    pub window: Weak<RefCell<Window>>,
    pub edge: crate::ssd::ResizeEdge,
    pub start_pointer: Point<f64, Logical>,
    pub start_rect: Rectangle<i32, Logical>,
}

#[derive(Debug)]
pub struct ResizeDragX11 {
    pub wl: WlSurface,
    pub edge: crate::ssd::ResizeEdge,
    pub start_pointer: Point<f64, Logical>,
    pub start_rect: Rectangle<i32, Logical>,
}

#[cfg(test)]
mod snap_tests {
    use super::SnapZone;
    use smithay::utils::{Logical, Point, Rectangle};

    fn avail() -> Rectangle<i32, Logical> {
        // 1920×1080 starting at (0, 28) — the topbar offset typical for the
        // built-in 28 px topbar. Tests exercise the absolute coordinates so
        // the available rect's origin matters, not just the size.
        Rectangle::new((0, 28).into(), (1920, 1080).into())
    }

    fn p(x: f64, y: f64) -> Point<f64, Logical> {
        Point::from((x, y))
    }

    // Bands: 480 px on the sides, 270 px top and bottom.
    const FRAC: f64 = 0.25;

    // ── detect ──

    #[test]
    fn detect_top_edge_by_thirds() {
        // Top edge band is 12 px below avail.loc.y (= 28); the position
        // along the edge, in thirds of the width (640 px each), picks the
        // quarter or the top half.
        assert_eq!(SnapZone::detect(p(320.0,  30.0), avail(), FRAC), Some(SnapZone::NW));
        assert_eq!(SnapZone::detect(p(960.0,  30.0), avail(), FRAC), Some(SnapZone::Top));
        assert_eq!(SnapZone::detect(p(1600.0, 30.0), avail(), FRAC), Some(SnapZone::NE));
    }

    #[test]
    fn detect_bottom_edge_by_thirds() {
        assert_eq!(SnapZone::detect(p(320.0,  1100.0), avail(), FRAC), Some(SnapZone::SW));
        assert_eq!(SnapZone::detect(p(960.0,  1100.0), avail(), FRAC), Some(SnapZone::Bottom));
        assert_eq!(SnapZone::detect(p(1600.0, 1100.0), avail(), FRAC), Some(SnapZone::SE));
    }

    #[test]
    fn detect_side_edges_by_thirds() {
        // Vertical thirds are 360 px each starting at y=28.
        assert_eq!(SnapZone::detect(p(5.0,    200.0), avail(), FRAC), Some(SnapZone::NW));
        assert_eq!(SnapZone::detect(p(5.0,    568.0), avail(), FRAC), Some(SnapZone::Left));
        assert_eq!(SnapZone::detect(p(5.0,    1000.0), avail(), FRAC), Some(SnapZone::SW));
        assert_eq!(SnapZone::detect(p(1915.0, 200.0), avail(), FRAC), Some(SnapZone::NE));
        assert_eq!(SnapZone::detect(p(1915.0, 568.0), avail(), FRAC), Some(SnapZone::Right));
        assert_eq!(SnapZone::detect(p(1915.0, 1000.0), avail(), FRAC), Some(SnapZone::SE));
    }

    #[test]
    fn detect_band_depth_follows_fraction() {
        // 300 px in from the left edge: inside the 25 % band, outside a
        // 10 % one.
        assert_eq!(SnapZone::detect(p(300.0, 568.0), avail(), FRAC), Some(SnapZone::Left));
        assert_eq!(SnapZone::detect(p(300.0, 568.0), avail(), 0.10), None);
    }

    #[test]
    fn detect_centre_returns_none() {
        assert_eq!(SnapZone::detect(p(960.0, 540.0), avail(), FRAC), None);
    }

    #[test]
    fn detect_true_corner_agrees_on_quarter() {
        // (5, 30) is near both the top and the left edge; both
        // classifications resolve to the same NW quarter.
        assert_eq!(SnapZone::detect(p(5.0, 30.0), avail(), FRAC), Some(SnapZone::NW));
        assert_eq!(SnapZone::detect(p(1915.0, 1100.0), avail(), FRAC), Some(SnapZone::SE));
    }

    // ── rect ──

    #[test]
    fn rect_top_bottom_partition_available() {
        let top = SnapZone::Top.rect(avail());
        let bottom = SnapZone::Bottom.rect(avail());
        assert_eq!(top.loc, avail().loc);
        assert_eq!(top.size.w, avail().size.w);
        assert_eq!(bottom.loc.y, top.loc.y + top.size.h);
        assert_eq!(top.size.h + bottom.size.h, avail().size.h);
    }

    #[test]
    fn rect_left_is_left_half() {
        let r = SnapZone::Left.rect(avail());
        assert_eq!(r.loc.x, 0);
        assert_eq!(r.loc.y, 28);
        assert_eq!(r.size.w, 960);
        assert_eq!(r.size.h, 1080);
    }

    #[test]
    fn rect_right_covers_remaining_width() {
        // Right half should reach the right edge — including any odd
        // pixel left over by integer division (`w - half_w`).
        let r = SnapZone::Right.rect(avail());
        assert_eq!(r.loc.x, 960);
        assert_eq!(r.size.w, 960);
        assert_eq!(r.loc.x + r.size.w, 1920);
    }

    #[test]
    fn rect_corners_partition_available() {
        // The four corners must perfectly tile the available rect — no
        // gap rows / columns from off-by-one rounding.
        let nw = SnapZone::NW.rect(avail());
        let ne = SnapZone::NE.rect(avail());
        let sw = SnapZone::SW.rect(avail());
        let se = SnapZone::SE.rect(avail());

        assert_eq!(nw.loc, avail().loc);
        assert_eq!(ne.loc.x, nw.loc.x + nw.size.w);
        assert_eq!(sw.loc.y, nw.loc.y + nw.size.h);
        assert_eq!(se.loc.x, sw.loc.x + sw.size.w);
        assert_eq!(se.loc.y, ne.loc.y + ne.size.h);

        // Total width / height of the four-quarter mosaic equals the
        // available rect's width / height.
        assert_eq!(nw.size.w + ne.size.w, avail().size.w);
        assert_eq!(nw.size.h + sw.size.h, avail().size.h);
    }
}
