//! Introspection and externally-driven commands: the IPC/switcher surface —
//! listing toplevels, activating / closing / resizing / minimizing windows by
//! id or app-id, moving windows between outputs, workspace switching and
//! switcher thumbnails. Operates on `Windows`.

use super::*;

impl Windows {
    /// Switch to a specific workspace by index (0-based).
    pub fn switch_workspace(&mut self, index: usize) {
        if self.layouts.is_empty() {
            return;
        }

        // Leave fullscreen through the restore path — dropping the view
        // directly would leave the client's Fullscreen state set and its
        // fullscreen rect in place, desyncing it until an F11 cycle.
        if matches!(self.view, View::Fullscreen(_)) {
            self.unfullscreen(None);
        }

        self.layouts.switch_to(&self.output, index);
        self.dirty = true;
    }

    /// True when no live windows remain in any workspace.
    pub fn has_no_windows(&self) -> bool
    {
        self.layouts.windows().all( |w| !w.alive() )
    }

    /// Get total number of workspaces.
    pub fn workspace_count(&self) -> usize {
        self.layouts.len()
    }

    /// Get active workspace index.
    pub fn active_workspace(&self) -> Option<usize> {
        self.layouts.active_index()
    }

    /// Close a window by app_id.
    pub fn close_by_app_id(&self, app_id: &str) {
        for layout in self.layouts.layouts() {
            for window in layout.windows() {
                let w = window.borrow();
                if w.app_id.as_deref() == Some(app_id) {
                    w.surface.send_close();
                    return;
                }
            }
        }
    }

    /// Close the currently focused toplevel window (Alt+F4).
    pub fn close_focused(&mut self) {
        let active = self.layouts.pending_active();
        if let Some(window) = active.primary() {
            window.borrow().surface.send_close();
        }
    }

    /// List all toplevel windows for the shell's overview/task switcher.
    pub fn list_toplevel_windows(&self) -> Vec<serde_json::Value> {
        let mut result = Vec::new();
        for layout in self.layouts.layouts() {
            for window in layout.windows() {
                let w = window.borrow();
                let wl = w.surface.surface();
                let id = wl.id().protocol_id() as u64;
                let app_id = w.app_id.clone().unwrap_or_default();

                // Get title from xdg-shell attributes
                let title = smithay::wayland::compositor::with_states(wl, |states| {
                    states
                        .data_map
                        .get::<smithay::wayland::shell::xdg::XdgToplevelSurfaceData>()
                        .and_then(|d| d.lock().ok())
                        .and_then(|d| d.title.clone())
                        .unwrap_or_default()
                });

                result.push(serde_json::json!({
                    "id": id,
                    "title": title,
                    "app_id": app_id,
                }));
            }
        }
        for entry in self.x11_windows.entries() {
            let Some(wl) = entry.surface.wl_surface() else { continue };
            result.push(serde_json::json!({
                "id": wl.id().protocol_id() as u64,
                "title": entry.surface.title(),
                "app_id": entry.surface.class(),
                "x11": true,
            }));
        }
        result
    }

    /// Activate (focus + raise) a window by its protocol ID.
    pub fn activate_window_by_id(&mut self, id: u64) {
        let mut target_ws = None;
        let mut target_window: Option<Rc<RefCell<Window>>> = None;
        for (ws_idx, layout) in self.layouts.layouts().iter().enumerate() {
            for window in layout.windows() {
                if window.borrow().surface.surface().id().protocol_id() as u64 == id {
                    target_ws = Some(ws_idx);
                    target_window = Some(window.clone());
                    break;
                }
            }
            if target_ws.is_some() { break; }
        }
        if let Some(ws_idx) = target_ws {
            let keep_fullscreen = match target_window.as_ref() {
                Some(w) => self.keep_or_unfullscreen(w),
                None => {
                    self.unfullscreen(None);
                    false
                }
            };
            if Some(ws_idx) != self.active_workspace() {
                self.switch_workspace(ws_idx);
            }
            // Raise the window in its workspace so it becomes primary.
            if let Some(w) = target_window {
                // Un-minimize on activation so the switcher can restore a
                // minimized entry.
                w.borrow_mut().minimized = false;
                let surface = w.borrow().surface.surface().clone();
                self.raise(&surface);
                self.set_focus(Some(FocusTarget::Layout(Rc::downgrade(&w))));
            }
            if !keep_fullscreen {
                self.set_view(View::Workspace);
            }
            self.dirty = true;
        }
    }

    /// Bring the first window matching `app_id` to the front. Drives
    /// activation directly off the `WlSurface` rather than the
    /// protocol id: `id().protocol_id()` is only unique within one
    /// Wayland client, so two clients can both have a surface whose
    /// `protocol_id` returns the same `u32`, and the by-id path would
    /// happily activate the wrong one.
    pub fn activate_window_by_app_id(&mut self, app_id: &str) {
        let target: Option<(usize, Rc<RefCell<Window>>)> = self.layouts.layouts().iter()
            .enumerate()
            .find_map(|(ws_idx, layout)| {
                layout.windows().find_map(|window| {
                    (window.borrow().app_id.as_deref() == Some(app_id))
                        .then(|| (ws_idx, window.clone()))
                })
            });
        if let Some((ws_idx, window_rc)) = target {
            let keep_fullscreen = self.keep_or_unfullscreen(&window_rc);
            if Some(ws_idx) != self.active_workspace() {
                self.switch_workspace(ws_idx);
            }
            window_rc.borrow_mut().minimized = false;
            let surface = window_rc.borrow().surface.surface().clone();
            self.raise(&surface);
            self.set_focus(Some(FocusTarget::Layout(Rc::downgrade(&window_rc))));
            if !keep_fullscreen {
                self.set_view(View::Workspace);
            }
            self.dirty = true;
            return;
        }

        let x11_match = self.x11_windows.entries().iter().find_map(|e| {
            (e.surface.class().as_str() == app_id
                || e.surface.instance().as_str() == app_id)
                .then(|| e.surface.wl_surface())
                .flatten()
        });
        if let Some(wl) = x11_match {
            self.unfullscreen(None);
            self.x11_windows.set_minimized(&wl, false);
            self.set_focus(Some(FocusTarget::X11(wl)));
            self.set_view(View::Workspace);
            self.dirty = true;
        }
    }

    /// Resize every Wayland toplevel whose `app_id` matches to `(w, h)`,
    /// preserving the current location. Used by `forge --winit --size`
    /// so the dev session can force its window to the requested size
    /// even when the host compositor has a saved geometry for that
    /// app_id in `geometry_store`.
    pub fn resize_window_by_app_id(&mut self, app_id: &str, w: i32, h: i32) {
        let scale = self.canvas.scale();
        let avail = self.output.available();
        let mut hit = false;
        for mut window in self.layouts.windows_mut() {
            if window.app_id.as_deref() == Some(app_id) {
                // Un-maximize first; `bar_h` is only consulted on the maximize
                // branch of `toggle_maximize`, which we never hit here because
                // the guard above means we are in the unmaximize branch.
                if window.is_maximized() {
                    window.toggle_maximize(scale, avail, 0);
                }
                // Keep the size but pull the origin into the work area: a
                // taller-than-screen window overflows at the bottom, never
                // above the reachable bar, which draws bar_h above the origin.
                let bar_h = if window.suppress_ssd(&self.csd_apps) { 0 }
                            else { crate::ssd::SsdDecoration::bar_height() };
                let top = avail.loc.y + bar_h;
                let mut loc = window.output_rectangle().loc;
                loc.x = loc.x.clamp(avail.loc.x, (avail.loc.x + avail.size.w - w).max(avail.loc.x));
                loc.y = loc.y.clamp(top, (avail.loc.y + avail.size.h - h).max(top));
                window.set_dimensions(scale, Rectangle::new(loc, (w, h).into()));
                // An explicit resize is manual control, like an interactive
                // one: keep the placeholder-floor fallback from overriding it.
                window.auto_sizing = false;
                hit = true;
            }
        }
        if hit {
            self.dirty = true;
        }
    }

    pub fn close_windows_by_app_id(&self, app_id: &str) {
        for layout in self.layouts.layouts() {
            for window in layout.windows() {
                let w = window.borrow();
                if w.app_id.as_deref() == Some(app_id) {
                    w.surface.send_close();
                }
            }
        }
        for entry in self.x11_windows.entries() {
            if entry.surface.class().as_str() == app_id
                || entry.surface.instance().as_str() == app_id
            {
                let _ = entry.surface.close();
            }
        }
    }

    /// Close a window by its protocol ID (sends xdg_toplevel.close).
    pub fn close_window_by_id(&self, id: u64) {
        for layout in self.layouts.layouts() {
            for window in layout.windows() {
                let w = window.borrow();
                if w.surface.surface().id().protocol_id() as u64 == id {
                    w.surface.send_close();
                    return;
                }
            }
        }
        for entry in self.x11_windows.entries() {
            let Some(wl) = entry.surface.wl_surface() else { continue };
            if wl.id().protocol_id() as u64 == id {
                let _ = entry.surface.close();
                return;
            }
        }
    }

    pub fn close_window_by_object_id(
        &self,
        oid: &smithay::reexports::wayland_server::backend::ObjectId,
    ) {
        for layout in self.layouts.layouts() {
            for window in layout.windows() {
                let w = window.borrow();
                if w.surface.surface().id() == *oid {
                    w.surface.send_close();
                    return;
                }
            }
        }
        for entry in self.x11_windows.entries() {
            let Some(wl) = entry.surface.wl_surface() else { continue };
            if wl.id() == *oid {
                let _ = entry.surface.close();
                return;
            }
        }
    }

    /// Minimize all windows in the currently active layout.
    pub fn minimize_all(&mut self) {
        for window in self.layouts.active().windows() {
            window.borrow_mut().minimized = true;
        }
        // Drop focus: with every window minimised there is nothing to type
        // into, so the focused app's text-input deactivates and the OSK comes
        // down (and `create_frame` won't re-focus the now-hidden window).
        self.set_focus(None);
        self.set_view(View::Workspace);
        self.dirty = true;
    }
}
