//! Window-state operations. Fullscreen entry/exit (taking and restoring the
//! pre-fullscreen geometry, re-fitting against the work area on exit).
//! Operates on `Windows`' layout and view.

use super::*;
use crate::geometry_store::OutputRef;

impl Windows {
    pub fn fullscreen(&mut self, surface: &ToplevelSurface) {
        if let Some(window) = self.layouts.find_window(surface.surface()) {
            // A window on an extend head fullscreens on its own screen and
            // never owns the view: the primary keeps its desktop, and other
            // windows keep working while e.g. a video fills the external.
            let owner_rect = {
                let r = window.borrow().output_rectangle();
                let owner = self.owner_output(r);
                (!self.is_primary(owner)).then(|| owner.logical_rect())
            };
            if let Some(rect) = owner_rect {
                let scale = self.output.scale();
                {
                    let mut w = window.borrow_mut();
                    w.enter_fullscreen(rect);
                    w.surface.set_state(|state| {
                        state.states.set(State::Fullscreen);
                    });
                    w.set_dimensions(scale, rect);
                }
                let wl = window.borrow().surface.surface().clone();
                self.z_promote(&wl);
                self.dirty = true;
                return;
            }

            let avail = self.output.available();
            {
                let mut w = window.borrow_mut();
                // Snapshot the pre-fullscreen rect so unfullscreen can land
                // the window back on its previous geometry. Floating windows
                // need this — Layout::resize skips them, so without the
                // snapshot they would keep the fullscreen rectangle and end
                // up covering the topbar after exiting.
                w.enter_fullscreen(avail);
                w.surface.set_state(|state| {
                    state.states.set(State::Fullscreen);
                });
            }

            self.set_view(View::Fullscreen(window.clone()));
            self.resize_visible();
        }
    }

    /// Activation helper: keep fullscreen when `target` is the fullscreened
    /// window itself; otherwise leave fullscreen through [`Self::unfullscreen`]
    /// so the client's Fullscreen state never outlives the view. Returns true
    /// when fullscreen was kept.
    pub(super) fn keep_or_unfullscreen(&mut self, target: &Rc<RefCell<Window>>) -> bool {
        if let View::Fullscreen(fs) = &self.view {
            if Rc::ptr_eq(fs, target) {
                return true;
            }
        }
        self.unfullscreen(None);
        false
    }

    pub fn unfullscreen<'a>(&mut self, surface: impl Into<Option<&'a ToplevelSurface>>) {
        let surface = surface.into();

        // A window fullscreened on an extend head never owns the view, so
        // resolve it by surface: unset the state and restore the snapshot,
        // re-fitting only if its screen's region moved in the meantime.
        if !matches!(&self.view, View::Fullscreen(_)) {
            let Some(s) = surface else { return };
            let Some(window) = self.layouts.find_window(s.surface()) else { return };
            let mut w = window.borrow_mut();
            let Some(prev_avail) = w.take_pre_fullscreen_avail() else { return };
            w.surface.set_state(|state| {
                state.states.unset(State::Fullscreen);
            });
            let scale = self.output.scale();
            w.exit_fullscreen(scale);
            let owner = self.owner_area(w.output_rectangle());
            if w.floating && prev_avail != owner {
                let bar_h = self.bar_h_for(&w);
                w.clamp_to_available(scale, owner, bar_h);
            }
            drop(w);
            self.dirty = true;
            return;
        }

        let View::Fullscreen(window) = &self.view else { return };
        let window_rc = window.clone();
        let mut window = window_rc.borrow_mut();

        if surface.is_none_or(|s| s == &window.surface) {
            window.surface.set_state(|state| {
                state.states.unset(State::Fullscreen);
            });
            // Hand the window back its pre-fullscreen rect before the
            // generic resize_visible path runs. resize_visible only re-tiles
            // tracked layout windows — floating windows would otherwise
            // keep the fullscreen rectangle (no topbar offset) and cover
            // the topbar permanently after a single F11.
            let scale = self.output.scale();
            let avail = self.output.available();
            let prev_avail = window.take_pre_fullscreen_avail();
            window.exit_fullscreen(scale);
            // Re-fit only when the work area moved while fullscreen (the
            // layout was skipped by resize_all, so the restored rect may
            // predate a scale / mode change); an unchanged area gets the
            // snapshot back untouched.
            if window.floating && prev_avail != Some(avail) {
                let bar_h = self.bar_h_for(&window);
                window.clamp_to_available(scale, avail, bar_h);
            }
            drop(window);

            self.set_view(View::Workspace);
            self.resize_visible();
        }
    }
}

// Maximize / minimize toggles and the SSD-bar geometry helpers.
impl Windows {
    /// Toggle SSD-maximize for the focused window (title-bar Max button).
    /// On desktop outputs this toggles the xdg Maximized state and resizes
    /// the window to the work area. On mobile falls back to fullscreen so
    /// the button still does something useful.
    /// Double-click on a title bar: cancel the drag the first press armed
    /// and toggle maximize on the window it actually grabbed — the X11
    /// drag target when one is active, the focused window otherwise.
    pub fn toggle_maximize_dragged(&mut self) {
        if let Some(drag) = self.move_drag_x11.take() {
            let avail = self.output.available();
            self.x11_windows.toggle_maximize(&drag.wl, avail);
            self.dirty = true;
            return;
        }
        self.end_move_drag();
        self.toggle_maximize_focused();
    }

    pub fn toggle_maximize_focused(&mut self) {
        if self.canvas.is_mobile() {
            self.toggle_fullscreen();
            return;
        }
        let active = self.layouts.pending_active();
        let Some(window) = active.primary() else { return };
        let avail = self.owner_area(window.borrow().output_rectangle());
        let scale = self.output.scale();
        let bar_h = self.bar_h_for(&window.borrow());
        window.borrow_mut().toggle_maximize(scale, avail, bar_h);
        self.record_geometry(&mut window.borrow_mut());
        let wl = window.borrow().surface.surface().clone();
        self.raise_toplevel(&wl);
        self.dirty = true;
    }

    pub(super) fn bar_h_for(&self, w: &Window) -> i32 {
        if w.suppress_ssd(&self.csd_apps) { 0 } else { crate::ssd::SsdDecoration::bar_height() }
    }

    /// Persist `w`'s floating geometry (or its pre-maximize rect when
    /// currently maximized) under `app_id` in the on-disk geometry
    /// store. Called from every user-driven geometry change so that
    /// the next launch of the same app can adopt the rect via the
    /// restore hook in `surface_commit`. No-op when `app_id` is
    /// missing or empty.
    pub(super) fn record_geometry(&mut self, w: &mut Window) {
        w.deferred_geo = None;
        let (rect, maximized, output) = self.record_session_geometry(w);
        // A child dialog shares the parent app's app_id; its rect must not
        // become the app's remembered geometry.
        if w.surface.parent().is_some() { return; }
        let Some(app_id) = w.app_id.as_deref() else { return };
        if app_id.is_empty() { return; }
        self.geometry_store.record(app_id, rect, maximized, output);
    }

    /// Record `w`'s WM state under its session binding, if any, and return
    /// what was recorded for the per-app store. A window still waiting for
    /// its saved screen keeps that saved state on record rather than the
    /// primary-clamped stand-in it currently occupies.
    pub(super) fn record_session_geometry(
        &mut self,
        w: &Window,
    ) -> (Rectangle<i32, Logical>, bool, OutputRef) {
        let (rect, maximized, output) = match &w.deferred_geo {
            Some(saved) => (saved.rect(), saved.maximized, saved.output_ref()),
            None => {
                let rect = w.persist_rect();
                let owner = self.owner_output(rect);
                let output = if self.is_primary(owner) {
                    OutputRef::default()
                } else {
                    OutputRef {
                        connector: Some(owner.smithay_output().name()),
                        identity: owner.identity().map(str::to_string),
                    }
                };
                (rect, w.is_maximized(), output)
            },
        };
        if let Some(s) = &w.session {
            let stack = self.z_index(w.surface.surface()).unwrap_or(0) as u32;
            self.session_store.record(
                &s.session_id, &s.name, rect, maximized, output.clone(), stack,
            );
        }
        (rect, maximized, output)
    }

    /// Toggle fullscreen for the focused window (F11).
    pub fn toggle_fullscreen(&mut self) {
        if let View::Fullscreen(_) = &self.view {
            self.unfullscreen(None);
        } else {
            let active = self.layouts.pending_active();
            if let Some(window) = active.primary() {
                let surface = window.borrow().surface.clone();
                // Fullscreen-on-an-extend-head does not own the view, so the
                // toggle must ask the window itself.
                if window.borrow().is_fullscreen() {
                    self.unfullscreen(&surface);
                } else {
                    self.fullscreen(&surface);
                }
            }
        }
    }

    /// Minimize only the focused window (SSD title-bar Min button).
    pub fn minimize_focused( &mut self )
    {
        let active = self.layouts.pending_active();
        if let Some( window ) = active.primary() {
            window.borrow_mut().minimized = true;
            self.dirty = true;
        }
    }
}

// Surface-driven maximize / minimize (Wayland + Xwayland) and the lookup helper.
impl Windows {
    pub fn set_maximized(&mut self, surface: &ToplevelSurface) {
        let Some(window_rc) = self.find_toplevel(surface) else { return };
        if window_rc.borrow().is_maximized() {
            return;
        }
        let avail = self.owner_area(window_rc.borrow().output_rectangle());
        let scale = self.output.scale();
        let bar_h = self.bar_h_for(&window_rc.borrow());
        window_rc.borrow_mut().toggle_maximize(scale, avail, bar_h);
        self.record_geometry(&mut window_rc.borrow_mut());
        let wl = window_rc.borrow().surface.surface().clone();
        self.raise_toplevel(&wl);
        self.dirty = true;
    }

    pub fn unset_maximized(&mut self, surface: &ToplevelSurface) {
        let Some(window_rc) = self.find_toplevel(surface) else { return };
        if !window_rc.borrow().is_maximized() {
            return;
        }
        window_rc.borrow_mut().unmaximize(self.output.scale());
        self.record_geometry(&mut window_rc.borrow_mut());
        let wl = window_rc.borrow().surface.surface().clone();
        self.raise_toplevel(&wl);
        self.dirty = true;
    }

    /// Bring a toplevel to the front in both the render z-order and the
    /// desktop hit-test stacking, without touching keyboard focus or the
    /// active layout. Used by client-driven (un)maximize so the window is
    /// not left buried behind another.
    pub(super) fn raise_toplevel(&mut self, surface: &WlSurface) {
        self.z_promote(surface);
        if let Some(id) = self.layouts.layout_id_for(surface) {
            self.layouts.raise_to_top(id);
        }
    }

    pub fn set_minimized(&mut self, surface: &ToplevelSurface) {
        let Some(window_rc) = self.find_toplevel(surface) else { return };
        window_rc.borrow_mut().minimized = true;
        self.dirty = true;
    }

    pub fn find_toplevel(&self, surface: &ToplevelSurface) -> Option<Rc<RefCell<Window>>> {
        self.layouts.layouts()
            .iter()
            .flat_map(|l| l.windows())
            .find(|w| &w.borrow().surface == surface)
            .cloned()
    }
    pub fn set_x11_maximized(
        &mut self,
        surface: &smithay::xwayland::X11Surface,
        maximized: bool,
    ) {
        let avail = self.output.available();
        self.x11_windows.set_maximized(surface, maximized, avail);
        self.dirty = true;
    }
}
