//! Surface lifecycle on `Windows`: mapping toplevels/layers/popups, surface
//! commits, buffer import and teardown.

use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;

use super::*;

impl Windows {
    pub fn add(&mut self, surface: ToplevelSurface, foreign_handle: Option<ForeignToplevelHandle>) {
        // Mobile spawns toplevels as full-canvas tiles, so it makes sense to
        // tell the client it's tiled on every edge (drops rounded corners and
        // shadows). On desktop the same hint is poison: GTK4/libadwaita and
        // other modern toolkits treat "fully tiled" as "this window is
        // constrained inside a tiling grid", hide their CSD resize handles
        // and refuse user resize. Desktop windows are floating, so the
        // initial state stays unset and the client gets to render its normal
        // resize handles.
        let is_mobile = self.canvas.is_mobile();
        surface.with_pending_state(|state| {
            if is_mobile {
                if surface.version() >= 2 {
                    state.states.set(State::TiledBottom);
                    state.states.set(State::TiledRight);
                    state.states.set(State::TiledLeft);
                    state.states.set(State::TiledTop);
                } else {
                    state.states.set(State::Maximized);
                }
            }

            state.states.set(State::Activated);
        });

        let transform = self.output.orientation().surface_transform();
        let scale = self.output.scale();

        let mut window_inner = Window::new(surface, scale, transform, None);
        window_inner.foreign_handle = foreign_handle;
        let window = Rc::new(RefCell::new(window_inner));

        // Desktop mode: new toplevels spawn as floating with a cascaded
        // start position and a reasonable default size. Major clients
        // (Firefox, Chromium, GTK apps) don't negotiate a deterministic
        // auto-size when the compositor sends size=None — they fall
        // back to a very small buffer — so we suggest an initial size
        // and let the client override via `xdg_surface.set_window_geometry`
        // on its first commit if it really wants something else. The
        // acked_size fallback added earlier in surface.rs still covers
        // well-behaved clients that pass their own geometry.
        if !self.canvas.is_mobile() {
            // New windows open on the screen holding the pointer; extras
            // carry no chrome, so their whole region is the work area.
            let avail = match self.pointer_pos {
                Some(p) => {
                    let target = self.output_at(p);
                    if target.smithay_output().name() == self.output.smithay_output().name() {
                        self.output.available()
                    } else {
                        target.logical_rect()
                    }
                }
                None => self.output.available(),
            };

            // Cascade position. Size is left at (0, 0): the initial
            // configure ships size = None so the client picks its
            // preferred footprint (xdg-shell convention). The first
            // surface_commit adopts `acked_size`; if the client
            // negotiated nothing reasonable, `default_floating_size`
            // backfills with 1024x720 and re-configures.
            // Clamp the assumed height to the output (mirroring the clamped
            // 1024x720 fallback in surface_commit_common) so outputs shorter
            // than 720 still get a usable cascade range instead of stacking
            // every window at the same +32,+32.
            let default_h: i32 = 720.min((avail.size.h - 64).max(1));
            let max_step = (avail.size.h - default_h).max(1);
            let step = self.next_cascade.rem_euclid(max_step);
            self.next_cascade = (self.next_cascade + 32).rem_euclid(max_step);

            let loc = Point::<i32, Logical>::from((
                avail.loc.x + 32 + step,
                avail.loc.y + 32 + step,
            ));

            let mut w = window.borrow_mut();
            w.floating = true;
            w.auto_sizing = true;
            w.set_floating_position(loc);
            // Mark for geometry restore on the first commit that resolves
            // a non-empty app_id. The restore reads from the on-disk store
            // populated by previous sessions (eydos #7). Tiled / mobile
            // windows are intentionally not flagged — their geometry is
            // owned by the layout, not the per-app saved rect.
            w.needs_geometry_restore = true;
        }

        let wl = window.borrow().surface.surface().clone();
        self.layouts.create(&self.output, window);
        self.z_promote(&wl);

        self.unfullscreen(None);
    }

    pub fn add_layer(
        &mut self,
        layer: Layer,
        surface: LayerSurface,
        namespace: String,
        wl_output: Option<&WlOutput>,
    ) {
        // A surface bound to an output other than the primary lives in that
        // output's local frame and sizes against it. The name is resolved
        // from the resource, not from `extra_outputs`: in mirror mode the
        // extra head does not exist, and falling back to the primary there
        // would stack a duplicate background on it — a named-but-absent
        // target is simply not rendered until its head appears.
        let name = wl_output
            .and_then(smithay::output::Output::from_resource)
            .map(|o| o.name())
            .filter(|n| *n != self.output.smithay_output().name());
        let output = name
            .as_deref()
            .and_then(|n| self.extra_outputs.iter().find(|o| o.smithay_output().name() == n))
            .unwrap_or(&self.output);
        let transform = output.orientation().surface_transform();
        let scale = output.scale();

        let surface = ForgeLayerSurface::new(layer, surface, name);
        let mut window = Window::new(surface, scale, transform, Some(namespace));

        window.enter(output);
        self.layers.add(window);
    }

    pub fn add_popup(&mut self, popup: PopupSurface) {
        let transform = self.output.orientation().surface_transform();
        let scale = self.output.scale();

        self.orphan_popups.push(Window::new(popup, scale, transform, None));
    }

    pub fn reposition_popup(&mut self, popup: &PopupSurface, token: u32) {
        let scale = self.output.scale();
        let rects = self.output_rects();
        for mut window in self.layouts.windows_mut() {
            // Constrain against the parent's own screen, expressed in that
            // screen's local frame so the popup flips at its edges.
            let rect = window.output_rectangle();
            let owner = Self::owner_rect_in(&rects, rect);
            let parent_loc = rect.loc - owner.loc;
            window.reposition_popup(scale, owner.size, parent_loc, popup, token);
        }
    }

    pub fn find_xdg(&mut self, wl_surface: &WlSurface) -> Option<RefMut<'_, Window>> {
        let mut root = Cow::Borrowed(wl_surface);
        while let Some(parent) = compositor::get_parent(&root) {
            root = Cow::Owned(parent);
        }

        self.layouts.windows_mut().find(|w| w.surface().eq(root.as_ref()))
    }

    pub fn surface_commit(&mut self, surface: &WlSurface) {
        let mut root = Cow::Borrowed(surface);
        while let Some(parent) = compositor::get_parent(&root) {
            root = Cow::Owned(parent);
        }

        macro_rules! find_window {
            ($windows:expr) => {
                $windows.find(|w| w.surface().eq(root.as_ref()))
            };
        }

        let scale = self.output.scale();
        let avail = self.output.available().size;

        if self.cursor_surface.as_ref() == Some(root.as_ref()) {
            crate::windows::window::take_surface_tree_buffers(root.as_ref());
            self.dirty = true;
            return;
        }

        if let View::Lock(Some(window)) = self.pending_view_mut() {
            if window.surface() == root.as_ref() {
                window.surface_commit_common(scale, avail, &[], surface);
                return;
            }
        }

        if !self.extra_locks.is_empty() {
            let sizes: Vec<(String, f64, Size<i32, Logical>)> = self
                .extra_outputs
                .iter()
                .map(|o| (o.smithay_output().name(), o.scale(), o.size()))
                .collect();
            for (name, window) in self.extra_locks.iter_mut() {
                if window.surface() == root.as_ref() {
                    let (s, sz) = sizes
                        .iter()
                        .find(|(n, _, _)| n == name)
                        .map(|(_, s, sz)| (*s, *sz))
                        .unwrap_or((scale, avail));
                    window.surface_commit_common(s, sz, &[], surface);
                    self.dirty = true;
                    return;
                }
            }
        }

        if let Some(mut window) = find_window!(self.layouts.windows_mut()) {
            // Session restore must be folded into the first configure, and
            // `restored` must precede it, so this runs before the commit path.
            if !window.surface.initial_configure_sent() {
                let pending = window
                    .session
                    .as_mut()
                    .and_then(|s| s.pending_restore.take());
                if let Some(saved) = pending {
                    let (min_size, max_size) = window.size_constraints();
                    let (avail, rect, resolved) = Self::saved_rect_target(
                        &self.extra_outputs, &self.output, &saved.geo, min_size, max_size,
                    );
                    window.set_dimensions(scale, rect);
                    if saved.geo.maximized && !window.is_maximized() {
                        let bar_h = if window.suppress_ssd(&self.csd_apps) { 0 }
                                    else { crate::ssd::SsdDecoration::bar_height() };
                        window.toggle_maximize(scale, avail, bar_h);
                    }
                    if !resolved {
                        window.deferred_geo = Some(saved.geo.clone());
                    }
                    window.needs_geometry_restore = false;
                    window.auto_sizing = false;
                    if let Some(h) = window.session.as_ref().and_then(|s| s.handle.upgrade().ok()) {
                        h.restored();
                    }
                }
            }
            window.surface_commit_common(scale, avail, &self.window_scales, surface);
            // Geometry-restore hook (eydos #7). The first commit that
            // brings a non-empty `app_id` consults the on-disk store
            // and adopts the saved rect; subsequent commits leave the
            // flag clear, so user-driven resizes are not clobbered.
            // Maximized is re-applied via toggle_maximize so the
            // pre_max_rect machinery works on the next un-maximize.
            if window.needs_geometry_restore {
                // A child dialog (xdg_toplevel.parent set) shares the parent
                // app's app_id, so the saved-rect lookup would resize it to the
                // main window. Skip the restore and let it keep its negotiated
                // size.
                if window.surface.parent().is_some() {
                    window.needs_geometry_restore = false;
                } else {
                    // Clearing the flag is gated on a *non-empty* app_id —
                    // the first few commits of some clients (Firefox is the
                    // canonical case) arrive before `xdg_toplevel.set_app_id`,
                    // and clearing the flag there would mean we never try
                    // the restore once the real app_id finally lands. Once
                    // we have an identifier we get one shot at applying the
                    // saved rect; the result of that shot is irreversible.
                    let resolved_id: Option<String> = window
                        .app_id.as_ref()
                        .filter(|s| !s.is_empty())
                        .cloned();
                    if let Some(app_id) = resolved_id {
                        // min == max declares a fixed-size window whose rect
                        // must not be overridden. ltk holds that pin only
                        // until its first configure, so a window that looks
                        // fixed before its first buffer is re-judged once it
                        // maps instead of being written off immediately.
                        let (min_size, max_size) = window.size_constraints();
                        let fixed = min_size.w > 0 && min_size.h > 0
                            && min_size == max_size;
                        let mapped = compositor::with_states(root.as_ref(), |states| {
                            states
                                .data_map
                                .get::<RefCell<ForgeSurfaceData>>()
                                .is_some_and(|d| d.borrow().buffer.is_some())
                        });
                        if !fixed || mapped {
                            window.needs_geometry_restore = false;
                        }
                        if !fixed {
                            if let Some(saved) = self.geometry_store.lookup(&app_id) {
                                let (avail, rect, resolved) = Self::saved_rect_target(
                                    &self.extra_outputs, &self.output, &saved, min_size, max_size,
                                );
                                window.set_dimensions(scale, rect);
                                if saved.maximized && !window.is_maximized() {
                                    let bar_h = if window.suppress_ssd(&self.csd_apps) { 0 }
                                                else { crate::ssd::SsdDecoration::bar_height() };
                                    window.toggle_maximize(scale, avail, bar_h);
                                }
                                if !resolved {
                                    window.deferred_geo = Some(saved);
                                }
                            }
                        }
                    }
                }
            }
            return;
        }

        self.orphan_surface_commit(&root);

        let output_size = self.output.size();
        let rects = self.output_rects();
        for mut window in self.layouts.windows_mut() {
            // Constrain against the parent's own screen, in its local frame.
            let rect = window.output_rectangle();
            let owner = Self::owner_rect_in(&rects, rect);
            let parent_loc = rect.loc - owner.loc;
            if window.popup_surface_commit(scale, owner.size, parent_loc, &root, surface) {
                return;
            }
        }
        for window in self.layers.iter_mut() {
            let parent_loc = window.output_rectangle().loc;
            if window.popup_surface_commit(scale, output_size, parent_loc, &root, surface) {
                return;
            }
        }

        if self.x11_windows.owns(&root) {
            self.x11_windows.surface_commit(surface);
            self.dirty = true;
            return;
        }

        let Some(window) = find_window!(self.layers.iter_mut()) else { return };

        let fullscreen_active = matches!(self.view, View::Fullscreen(_));

        // A layer bound to an extra output sizes against that output and
        // never touches the primary's exclusive zones or focus.
        if let Some(name) = window.surface.target_output.clone() {
            if let Some(output) = self
                .extra_outputs
                .iter_mut()
                .find(|o| o.smithay_output().name() == name)
            {
                window.surface_commit(&self.window_scales, output, fullscreen_active, surface);
                self.dirty = true;
            }
            return;
        }

        let old_exclusive = *self.output.exclusive();
        window.surface_commit(&self.window_scales, &mut self.output, fullscreen_active, surface);

        // Auto-focus layer surfaces that request exclusive keyboard interactivity
        let exclusive_focus = if !window.deny_focus {
            let interactivity = compositor::with_states(window.surface.surface(), |states| {
                states
                    .cached_state
                    .get::<LayerSurfaceCachedState>()
                    .current()
                    .keyboard_interactivity
            });
            if interactivity == KeyboardInteractivity::Exclusive {
                Some((window.surface.surface().clone(), window.app_id.clone()))
            } else {
                None
            }
        } else {
            None
        };

        if let Some((surface, app_id)) = exclusive_focus {
            let same = matches!(&self.focus_target, Some(FocusTarget::Layer(s, _)) if s == &surface);
            if !same {
                self.set_focus(Some(FocusTarget::Layer(surface, app_id)));
            }
        }

        if self.output.exclusive() != &old_exclusive {
            self.resize_visible();
        }
    }

    pub fn orphan_surface_commit(&mut self, root: &WlSurface) -> Option<()> {
        let index = self.orphan_popups.iter().position(|p| p.surface() == root)?;
        let mut popup = self.orphan_popups.swap_remove(index);
        let parent = popup.parent()?;

        let active = self.layouts.active();
        if let Some(primary) = active.primary() {
            popup = primary.borrow_mut().add_popup(popup, &parent)?;
        }

        if let Some(secondary) = active.secondary() {
            popup = secondary.borrow_mut().add_popup(popup, &parent)?;
        }

        for window in self.layers.iter_mut() {
            popup = window.add_popup(popup, &parent)?;
        }

        popup.surface.send_popup_done();

        Some(())
    }

    #[cfg_attr(feature = "profiling", profiling::function)]
    pub fn import_buffers(&mut self, renderer: &mut GlesRenderer) {
        if let Some(surface) = self.cursor_surface.clone() {
            if surface.is_alive() {
                import_cursor_surface(renderer, &surface);
            } else {
                self.cursor_surface = None;
            }
        }

        for mut window in self.layouts.windows_mut() {
            window.import_buffers(renderer);
        }

        for window in self.layers.iter_mut() {
            window.import_buffers(renderer);
        }

        if let View::Lock(Some(window)) = &mut self.view {
            window.import_buffers(renderer);
        }

        for (_, window) in self.extra_locks.iter_mut() {
            window.import_buffers(renderer);
        }

        // Pull buffers from every mapped X11Surface's wl_surface tree.
        // Empty `x11_windows` is a no-op fast path.
        let scale = self.output.scale();
        self.x11_windows.import_buffers(renderer, scale);
    }


    pub fn reap_xdg(&mut self, surface: &ToplevelSurface) {
        // The last rect a session-managed window had is what a relaunch
        // should come back to, whether or not the user ever dragged it.
        if let Some(w) = self.find_toplevel(surface) {
            let w = w.borrow();
            if w.session.is_some() {
                self.record_session_geometry(&w);
            }
        }
        self.z_remove(surface.wl_surface());
        self.unfullscreen(surface);

        // Capture a tombstone snapshot *before* the layout reap drops the
        // window — `Layouts::reap` releases the only `Rc<RefCell<Window>>`
        // on the layout side, after which the texture cache is gone and
        // we'd have nothing to fade out. The snapshot survives because
        // `RenderTexture` is `Rc<Texture>`: cloning it bumps the refcount
        // and the GL texture lives until the closing animation completes.
        // The animation runs in both mobile and desktop mode (#3).
        if let Some(tombstone) = self.snapshot_for_close(surface) {
            self.closing_windows.push(tombstone);
        }

        self.layouts.reap(&self.output, surface);
    }

    pub fn reap_layer(&mut self, surface: &LayerSurface) {
        start_transaction();

        let old_exclusive = *self.output.exclusive();
        if let Some(window) = self.layers.iter().find(|l| l.surface.eq(surface)) {
            self.output.exclusive().reset(window.surface.anchor, window.surface.exclusive_zone);
        }

        if matches!(&self.focus_target, Some(FocusTarget::Layer(s, _)) if s == surface.wl_surface()) {
            self.focus_target = None;
        }

        if self.output.exclusive() != &old_exclusive {
            self.resize_visible();
        }
    }

    pub fn refresh_popups(&mut self) {
        for mut window in self.layouts.windows_mut() {
            window.refresh_popups();
        }
    }

    pub fn surface_is_layer(&self, surface: &WlSurface) -> bool {
        self.layers.iter().any(|l| l.surface() == surface)
    }


}
