// xfwl4 -- Wayland compositor for the Xfce Desktop Environment
//
// Copyright (C) 2026 Brian Tarricone <brian@tarricone.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// Portions of this file are based on "anvil", an example compositor
// based on the smithay crate, and are licensed under the MIT license
// with the following terms:
//
// Copyright (C) Victor Berger <victor.berger@m4x.org>
// Copyright (C) Drakulix (Victoria Brekenfeld)
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

use std::{
    os::unix::io::OwnedFd,
    sync::{Mutex, MutexGuard},
};

use smithay::{
    desktop::{Window, WindowSurface, space::SpaceElement},
    reexports::{
        calloop::{
            RegistrationToken,
            timer::{TimeoutAction, Timer},
        },
        rustix::{process, system},
        wayland_server::protocol::wl_surface::WlSurface,
    },
    utils::{Logical, Rectangle, SERIAL_COUNTER, Size},
    wayland::{
        seat::WaylandFocus,
        selection::{
            SelectionTarget,
            data_device::{
                clear_data_device_selection, current_data_device_selection_userdata, request_data_device_client_selection,
                set_data_device_selection,
            },
            primary_selection::{
                clear_primary_selection, current_primary_selection_userdata, request_primary_client_selection, set_primary_selection,
            },
        },
        xwayland_keyboard_grab::XWaylandKeyboardGrabHandler,
        xwayland_shell::{XWaylandShellHandler, XWaylandShellState},
    },
    xwayland::{
        X11Surface, X11Wm, XwmHandler,
        xwm::{
            MwmDecorationsHint, MwmFunctionsHint, MwmInputMode, PingError, Reorder, ResizeEdge as X11ResizeEdge, WmWindowProperty,
            WmWindowType, XwmId,
        },
    },
};
use tracing::{error, trace};

use crate::{
    backend::Backend,
    core::{
        config::ActivateAction,
        focus::KeyboardFocusTarget,
        placement::{FillMode, StackResult},
        shell::{GrabTrigger, WINDOW_PING_TIMEOUT},
        state::{WindowClient, Xfwl4Core, Xfwl4State},
    },
    protocols::foreign_toplevel_management::ToplevelChangedInput,
};

use super::{WindowCapabilities, WindowElement, WindowLayout};

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct X11ClientId(pub u32);

#[derive(Debug, Default)]
pub struct X11WindowPropsInner {
    ping_timeout_token: Option<RegistrationToken>,
}

#[derive(Debug, Default)]
pub struct X11WindowProps(pub Mutex<X11WindowPropsInner>);

impl<BackendData: Backend> XWaylandShellHandler for Xfwl4State<BackendData> {
    fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState {
        &mut self.core.shell_state.xwayland_shell_state
    }
}

impl<BackendData: Backend> XwmHandler for Xfwl4State<BackendData> {
    fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm {
        self.core.xwayland_state.x11_mut().unwrap().xwm()
    }

    fn new_window(&mut self, _xwm: XwmId, surface: X11Surface) {
        let internal_window_id = self.core.next_window_id();

        if let Some(xw) = self.core.xwayland_state.x11_mut() {
            surface
                .user_data()
                .insert_if_missing(|| X11ClientId(surface.window_id() & xw.client_resource_mask()));
            let window = WindowElement::new(Window::new_x11_window(surface.clone()), internal_window_id, &self.core.config);

            if let Err(err) = xw.init_window_as_pending(window) {
                tracing::info!("Failed to add new pending X11 window: {err}");
            }
        }
    }

    fn new_override_redirect_window(&mut self, _xwm: XwmId, surface: X11Surface) {
        let internal_window_id = self.core.next_window_id();

        if let Some(xw) = self.core.xwayland_state.x11_mut() {
            surface
                .user_data()
                .insert_if_missing(|| X11ClientId(surface.window_id() & xw.client_resource_mask()));
            let window = WindowElement::new(Window::new_x11_window(surface.clone()), internal_window_id, &self.core.config);

            if let Err(err) = xw.init_window_as_pending(window) {
                tracing::info!("Failed to add new pending X11 window: {err}");
            }
        }
    }

    fn map_window_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(window) = self
            .core
            .xwayland_state
            .x11_mut()
            .and_then(|xw| xw.remove_pending_window(surface.window_id()))
            .or_else(|| {
                self.core
                    .workspace_manager
                    .find_window(|elem| matches!(elem.0.x11_surface(), Some(s) if s == &surface))
            })
        {
            let parent = surface.is_transient_for().and_then(|window_id| {
                self.core.workspace_manager.active_workspace().find_window(
                    |elem| matches!(elem.0.underlying_surface(), WindowSurface::X11(surface) if surface.window_id() == window_id),
                )
            });

            let _ = surface.set_mapped(true);

            self.set_window_parent(&window, parent.clone());

            window
                .props()
                .window_icon
                .update_app_id(Some(surface.class()).filter(|s| !s.is_empty()));
            self.core.xwayland_state.update_window_icon(&window);

            if window.wants_decorations() {
                self.enable_decorations_for_window(&window);
            } else {
                self.disable_decorations_for_window(&window);
            }

            let content_size = x11_window_content_size(&surface);

            let StackResult {
                location,
                allow_activate,
                needs_attention,
            } = self.stack_new_window(&window);
            self.place_window(&window, content_size, location, allow_activate);

            if needs_attention {
                self.set_window_urgent_state(&window, true);
            }

            let workspace = self.core.workspace_manager.active_workspace_mut();
            if let Some(loc) = workspace.window_location(&window) {
                let visible_rect = Rectangle::new(loc, content_size);
                let mut buffer_rect = window.grow_rect_by_gtk_frame_extents(visible_rect);
                buffer_rect.size = self.x11_constrain_to_size_hints(&surface, buffer_rect.size);
                let _ = surface.configure(Some(buffer_rect));
            }

            if surface.is_maximized() {
                // TODO: support vert vs. horiz (needs smithay work)
                self.set_window_maximized(&window, FillMode::Both, None);
            }
            if surface.is_shaded() {
                self.set_window_shaded(&window, true);
            }
            if surface.is_sticky() {
                self.set_window_sticky(&window, true);
            }
            if surface.is_hidden() {
                self.set_window_minimized(&window);
            }

            self.core.toplevel_created::<Self>(&window);

            self.update_window_capabilities(&window);
        }
    }

    fn mapped_override_redirect_window(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(window) = self
            .core
            .xwayland_state
            .x11_mut()
            .and_then(|xw| xw.remove_pending_window(surface.window_id()))
            .or_else(|| {
                self.core
                    .workspace_manager
                    .find_window(|elem| matches!(elem.0.x11_surface(), Some(s) if s == &surface))
            })
        {
            let location = surface.last_configure().loc;
            self.new_window(window, location, false, None);
        }
    }

    fn unmapped_window(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(window) = self
            .core
            .workspace_manager
            .find_window(|window| matches!(window.0.x11_surface(), Some(surf) if *surf == surface))
        {
            window.handle_destroyed();
            self.remove_window(&window);
            self.core.toplevel_destroyed(&window);
            if let Some(xw) = self.core.xwayland_state.x11_mut()
                && let Err(err) = xw.init_window_as_pending(window)
            {
                tracing::info!("Failed to add new pending X11 window: {err}");
            }
        }

        if !surface.is_override_redirect() {
            surface.set_mapped(false).unwrap();
        }
    }

    fn destroyed_window(&mut self, _xwm: XwmId, surface: X11Surface) {
        let target_id = surface.window_id();
        let found = self
            .core
            .workspace_manager
            .find_window(|elem| matches!(elem.0.underlying_surface(), WindowSurface::X11(s) if s.window_id() == target_id));
        if let Some(window) = found {
            window.handle_destroyed();
            self.remove_window(&window);
            self.core.toplevel_destroyed(&window);

            if let Some(xw) = self.core.xwayland_state.x11() {
                let client_mask = xw.client_resource_mask();
                let surface_client_id = surface.window_id() & client_mask;
                let has_remaining = self.core.workspace_manager.workspaces().iter().any(|workspace| {
                    workspace.all_windows().any(
                        |w| matches!(w.0.underlying_surface(), WindowSurface::X11(s) if s.window_id() & client_mask == surface_client_id),
                    )
                });
                if !has_remaining {
                    self.core.remove_window_client(WindowClient::X11(surface_client_id));
                }
            }
        } else {
            let _ = self
                .core
                .xwayland_state
                .x11_mut()
                .and_then(|xw| xw.remove_pending_window(target_id));
        }

        // X11Wm will re-set window stacking on window destroy, which will be incorrect, because
        // X11Wm doesn't actually know the correct stacking order.  `self.remove_window()` above
        // should fix it up, but let's be safe.
        self.x11_update_window_stacking_order();
    }

    fn configure_request(
        &mut self,
        _xwm: XwmId,
        surface: X11Surface,
        x: Option<i32>,
        y: Option<i32>,
        w: Option<u32>,
        h: Option<u32>,
        _reorder: Option<Reorder>,
    ) {
        let surface_geometry = surface.last_configure();

        let location = if (surface.is_override_redirect()
            || surface
                .window_type()
                .is_some_and(|ty| !matches!(ty, WmWindowType::Normal | WmWindowType::Dialog)))
            && (x.is_some() || y.is_some())
        {
            // Allow these sorts of windows to set their own position.

            if let Some((window, _, workspace)) = self
                .core
                .workspace_manager
                .find_window_and_workspace_mut(|elem| elem.0.x11_surface() == Some(&surface))
                && let Some(location) = workspace.window_location(&window)
            {
                let location = (x.unwrap_or(location.x), y.unwrap_or(location.y)).into();
                self.core.workspace_manager.relocate_window(&window, location);
                location
            } else {
                // Maybe it's a pending window.
                (x.unwrap_or(surface_geometry.loc.x), y.unwrap_or(surface_geometry.loc.y)).into()
            }
        } else {
            // Other kinds of windows don't get to move around freely.
            surface_geometry.loc
        };

        let configure_geometry = Rectangle::new(
            location,
            (
                w.unwrap_or(surface_geometry.size.w as u32) as i32,
                h.unwrap_or(surface_geometry.size.h as u32) as i32,
            )
                .into(),
        );
        let _ = surface.configure(configure_geometry);
    }

    fn configure_notify(&mut self, _xwm: XwmId, window: X11Surface, geometry: Rectangle<i32, Logical>, above: Option<u32>) {
        if let Some(elem) = self
            .core
            .workspace_manager
            .find_window(|elem| matches!(elem.0.x11_surface(), Some(w) if w == &window))
        {
            // `geometry.loc` is the X11 rect origin.  For CSD X11 windows the Space
            // position represents the visible-content origin (so smithay's
            // `render_location = Space - geometry().loc` cancels the extents offset
            // back out), so shift inward by the frame extents before relocating.
            let mut new_loc = geometry.loc;
            let frame_extents = window.frame_extents();
            new_loc.x += frame_extents.left;
            new_loc.y += frame_extents.top;
            self.core.workspace_manager.relocate_window(&elem, new_loc);

            if window.is_override_redirect() {
                match above {
                    None => self.core.workspace_manager.x11_restack_override_redirect(&elem, None),
                    // A sibling we can't resolve (e.g. not mapped yet) is left as-is, not sunk to the bottom.
                    Some(sibling_id) => {
                        if let Some(sibling) = self
                            .core
                            .workspace_manager
                            .find_window(|elem| matches!(elem.0.x11_surface(), Some(w) if w.window_id() == sibling_id))
                        {
                            self.core.workspace_manager.x11_restack_override_redirect(&elem, Some(&sibling));
                        }
                    }
                }
            }
        }
    }

    fn minimize_request(&mut self, _xwm: XwmId, window: X11Surface) {
        if let Some(window) = self
            .core
            .workspace_manager
            .find_window(|e| matches!(e.0.x11_surface(), Some(w) if w == &window))
        {
            self.set_window_minimized(&window);
        }
    }

    fn unminimize_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(window) = self
            .core
            .workspace_manager
            .find_window(|e| matches!(e.0.x11_surface(), Some(w) if w == &surface))
        {
            self.set_window_unminimized(&window, SERIAL_COUNTER.next_serial(), false);
        }
    }

    fn maximize_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(wl_surface) = surface.wl_surface()
            && let Some(window) = self.window_for_surface(&wl_surface)
        {
            self.set_window_maximized(&window, FillMode::Both, None);
        }
    }

    fn unmaximize_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(wl_surface) = surface.wl_surface()
            && let Some(window) = self.window_for_surface(&wl_surface)
        {
            self.set_window_unmaximized(&window, None);
        }
    }

    fn fullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {
        if let Some(elem) = self
            .core
            .workspace_manager
            .active_workspace()
            .find_window(|e| matches!(e.0.x11_surface(), Some(w) if w == &window))
        {
            self.set_window_fullscreen(&elem, None);
        }
    }

    fn unfullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {
        // This is kinda dumb, but keeps the borrow checker happy
        if let Some(window) = self
            .core
            .workspace_manager
            .find_window(|e| matches!(e.0.x11_surface(), Some(w) if w == &window))
        {
            self.set_window_unfullscreen(&window);
        }
    }

    fn modal_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        self.set_x11_window_modal(surface, true);
    }

    fn unmodal_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        self.set_x11_window_modal(surface, false);
    }

    fn above_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(wl_surface) = surface.wl_surface()
            && let Some(window) = self.window_for_surface(&wl_surface)
        {
            self.set_window_always_on_top(&window);
        }
    }

    fn unabove_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(wl_surface) = surface.wl_surface()
            && let Some(window) = self.window_for_surface(&wl_surface)
        {
            self.set_window_normal_stacking(&window);
        }
    }

    fn below_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(wl_surface) = surface.wl_surface()
            && let Some(window) = self.window_for_surface(&wl_surface)
        {
            self.set_window_always_on_bottom(&window);
        }
    }

    fn unbelow_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(wl_surface) = surface.wl_surface()
            && let Some(window) = self.window_for_surface(&wl_surface)
        {
            self.set_window_normal_stacking(&window);
        }
    }

    fn shade_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(wl_surface) = surface.wl_surface()
            && let Some(window) = self.window_for_surface(&wl_surface)
        {
            self.set_window_shaded(&window, true);
        }
    }

    fn unshade_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(wl_surface) = surface.wl_surface()
            && let Some(window) = self.window_for_surface(&wl_surface)
        {
            self.set_window_shaded(&window, false);
        }
    }

    fn stick_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(wl_surface) = surface.wl_surface()
            && let Some(window) = self.window_for_surface(&wl_surface)
        {
            self.set_window_sticky(&window, true);
        }
    }

    fn unstick_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(wl_surface) = surface.wl_surface()
            && let Some(window) = self.window_for_surface(&wl_surface)
        {
            self.set_window_sticky(&window, false);
        }
    }

    fn demands_attention_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(wl_surface) = surface.wl_surface()
            && let Some(window) = self.window_for_surface(&wl_surface)
            && !window.active()
        {
            self.set_window_urgent_state(&window, true);
        }
    }

    fn undemands_attention_request(&mut self, _xwm: XwmId, surface: X11Surface) {
        if let Some(wl_surface) = surface.wl_surface()
            && let Some(window) = self.window_for_surface(&wl_surface)
        {
            self.set_window_urgent_state(&window, false);
        }
    }

    fn resize_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32, edges: X11ResizeEdge) {
        if let Some(wl_surface) = window.wl_surface()
            && let Some(window) = self.window_for_surface(&wl_surface)
        {
            self.start_window_resize(
                window,
                self.core.seat.clone(),
                SERIAL_COUNTER.next_serial(),
                edges.into(),
                GrabTrigger::Pointer,
            );
        }
    }

    fn move_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32) {
        if let Some(wl_surface) = window.wl_surface()
            && let Some(window) = self.window_for_surface(&wl_surface)
        {
            self.start_window_move(window, self.core.seat.clone(), SERIAL_COUNTER.next_serial(), GrabTrigger::Pointer);
        }
    }

    fn active_window_request(&mut self, _xwm: XwmId, surface: X11Surface, _timestamp: u32, currently_active_window: Option<X11Surface>) {
        // Smithay doesn't expose the 'source' field of the _NET_ACTIVE_WINDOW message, which
        // can tell us if the source was a pager application.  An X11 WM might unconditionally
        // allow the activation request for pagers, but that field can of course be spoofed.  Here
        // we are going to deviate from that behavior, because on Wayland, it would be surprising
        // if the pager app was an X11 app and not a Wayland app using foreign-toplevel-management
        // to do the activation.  (And if we did have an X11 pager app, it would only be able to
        // see other X11 windows, and not be that useful anyway.)

        let currently_active_window = currently_active_window.and_then(|caw| {
            self.core
                .workspace_manager
                .find_window(|elem| elem.0.x11_surface().is_some_and(|surf| *surf == caw))
        });

        if let Some(wl_surface) = surface.wl_surface()
            && let Some((window, _, workspace)) = self
                .core
                .workspace_manager
                .find_window_and_workspace_mut(|elem| elem.0.wl_surface().is_some_and(|surf| surf.as_ref() == &wl_surface))
        {
            if currently_active_window.is_some_and(|caw| window.same_client_as(&caw)) {
                self.activate_window(&window, self.core.config.raise_on_focus(), self.core.config.activate_action(), None);
            } else if self.core.config.prevent_focus_stealing()
                && (self.core.input_state.user_interacted_since(window.last_user_interaction())
                    || self.core.config.activate_action() == ActivateAction::None)
            {
                if let Some(topmost_window) = workspace.visible_windows().last().cloned() {
                    workspace.lower_window_below(&window, &topmost_window);
                } else {
                    self.raise_window(&window, SERIAL_COUNTER.next_serial(), false);
                }
            } else {
                self.set_window_urgent_state(&window, true);
                let current_focus = self.core.seat.get_keyboard().and_then(|keyboard| keyboard.current_focus());

                if current_focus != Some(window.clone().into()) {
                    self.activate_window(&window, self.core.config.raise_on_focus(), self.core.config.activate_action(), None);
                }
            }
        }
    }

    fn allow_selection_access(&mut self, xwm: XwmId, _selection: SelectionTarget) -> bool {
        if let Some(keyboard) = self.core.seat.get_keyboard() {
            // check that an X11 window is focused
            if let Some(KeyboardFocusTarget::Window(w)) = keyboard.current_focus()
                && let Some(surface) = w.x11_surface()
                && surface.xwm_id().as_ref().is_some_and(|id| id == &xwm)
            {
                return true;
            }
        }
        false
    }

    fn send_selection(&mut self, _xwm: XwmId, selection: SelectionTarget, mime_type: String, fd: OwnedFd) {
        match selection {
            SelectionTarget::Clipboard => {
                if let Err(err) = request_data_device_client_selection(&self.core.seat, mime_type, fd) {
                    error!(?err, "Failed to request current wayland clipboard for Xwayland",);
                }
            }
            SelectionTarget::Primary => {
                if let Err(err) = request_primary_client_selection(&self.core.seat, mime_type, fd) {
                    error!(?err, "Failed to request current wayland primary selection for Xwayland",);
                }
            }
        }
    }

    fn new_selection(&mut self, _xwm: XwmId, selection: SelectionTarget, mime_types: Vec<String>) {
        trace!(?selection, ?mime_types, "Got Selection from X11",);
        // TODO check, that focused windows is X11 window before doing this
        match selection {
            SelectionTarget::Clipboard => set_data_device_selection(&self.core.display_handle, &self.core.seat, mime_types, ()),
            SelectionTarget::Primary => set_primary_selection(&self.core.display_handle, &self.core.seat, mime_types, ()),
        }
    }

    fn cleared_selection(&mut self, _xwm: XwmId, selection: SelectionTarget) {
        match selection {
            SelectionTarget::Clipboard => {
                if current_data_device_selection_userdata(&self.core.seat).is_some() {
                    clear_data_device_selection(&self.core.display_handle, &self.core.seat)
                }
            }
            SelectionTarget::Primary => {
                if current_primary_selection_userdata(&self.core.seat).is_some() {
                    clear_primary_selection(&self.core.display_handle, &self.core.seat)
                }
            }
        }
    }

    fn show_desktop_request(&mut self, _xwm: XwmId) {
        self.activate_show_desktop();
    }

    fn unshow_desktop_request(&mut self, _xwm: XwmId) {
        self.deactivate_show_desktop();
    }

    fn property_notify(&mut self, _xwm: XwmId, surface: X11Surface, property: WmWindowProperty) {
        if let Some(window) = surface.wl_surface().and_then(|surf| self.window_for_surface(&surf)) {
            match property {
                WmWindowProperty::Title => self.core.toplevel_changed(
                    &window,
                    ToplevelChangedInput {
                        title: Some(surface.title()),
                        ..Default::default()
                    },
                ),
                WmWindowProperty::Class => self.core.toplevel_changed(
                    &window,
                    ToplevelChangedInput {
                        app_id: Some(surface.class()),
                        ..Default::default()
                    },
                ),
                WmWindowProperty::TransientFor => {
                    if let Some(workspace) = self.core.workspace_manager.workspace_for_window_mut(&window) {
                        let parent = surface.is_transient_for().and_then(|window_id| {
                            workspace.find_window(|elem| matches!(elem.0.underlying_surface(), WindowSurface::X11(surface) if surface.window_id() == window_id))
                        });

                        self.set_window_parent(&window, parent.clone());

                        let parent_id = Some(parent.as_ref().and_then(|parent| self.core.toplevel_id_for_window(parent)));
                        self.core.toplevel_changed(
                            &window,
                            ToplevelChangedInput {
                                parent: parent_id,
                                ..Default::default()
                            },
                        );
                    }
                }
                WmWindowProperty::Hints => {
                    let urgent = surface.hints().map(|hints| hints.urgent);
                    self.set_window_urgent_state(&window, urgent.unwrap_or(false));
                }
                WmWindowProperty::NormalHints => self.update_window_capabilities(&window),
                WmWindowProperty::MotifHints => {
                    if window.wants_decorations() {
                        self.enable_decorations_for_window(&window);
                    } else {
                        self.disable_decorations_for_window(&window);
                    }
                    self.update_window_capabilities(&window);
                }
                WmWindowProperty::FrameExtents => {
                    // The frame extents (shadow widths) changed, so a tiled window's
                    // visible-content edge may no longer line up with its anchor.  Re-apply
                    // the anchored layout to keep it snapped.
                    let layout = window.current_layout();
                    if layout != WindowLayout::Normal {
                        let output_and_geom = window
                            .props()
                            .anchored_output
                            .as_ref()
                            .and_then(|weak| weak.upgrade())
                            .and_then(|output| self.core.workspace_manager.output_geometry(&output).map(|geom| (output, geom)));
                        if let Some((output, output_geom)) = output_and_geom
                            && self.apply_anchored_layout(&window, layout, &output, output_geom).is_none()
                        {
                            self.set_window_untiled(&window, None);
                        }
                    }
                }
                // TODO: need to manually add a property notify for _NET_WM_STATE
                _ => (),
            }
        }
    }

    fn ping_acked(&mut self, _xwm: XwmId, surface: X11Surface, _timestamp: u32) {
        if let Some(token) = surface
            .user_data()
            .get_or_insert(X11WindowProps::default)
            .0
            .lock()
            .unwrap()
            .ping_timeout_token
            .take()
        {
            self.core.loop_handle.remove(token);
        }

        if let Some(pos) = self
            .core
            .shell_state
            .not_responding_dialogs
            .iter()
            .position(|(_, dialog_window)| matches!(dialog_window.0.x11_surface(), Some(s) if *s == surface))
        {
            let (dialog_id, _) = self.core.shell_state.not_responding_dialogs.remove(pos);
            self.core.compositor_ui_state.cancel_dialog(dialog_id);
        }
    }

    fn disconnected(&mut self, _xwm: XwmId) {
        if let Some(display_number) = self.xwayland_destroyed() {
            self.maybe_schedule_xwayland_restart(display_number);
        }
    }
}

impl<BackendData: Backend + 'static> XWaylandKeyboardGrabHandler for Xfwl4State<BackendData> {
    fn keyboard_focus_for_xsurface(&self, surface: &WlSurface) -> Option<KeyboardFocusTarget> {
        let elem = self
            .core
            .workspace_manager
            .active_workspace()
            .visible_windows()
            .find(|elem: &&WindowElement| elem.wl_surface().as_deref() == Some(surface))?;
        Some(KeyboardFocusTarget::Window(elem.0.clone()))
    }
}

impl<BackendData: Backend> Xfwl4State<BackendData> {
    fn set_x11_window_modal(&mut self, surface: X11Surface, modal: bool) {
        if surface.is_modal() != modal {
            let _ = surface.set_modal(modal);

            if let Some(wl_surface) = surface.wl_surface()
                && let Some(window) = self.window_for_surface(&wl_surface)
            {
                self.update_window_capabilities(&window);
            }
        }
    }

    pub(in crate::core) fn x11_constrain_to_size_hints(&self, surface: &X11Surface, requested: Size<i32, Logical>) -> Size<i32, Logical> {
        let mut size = requested;

        let min = surface.min_size();
        let max = surface.max_size();
        let base = surface.base_size();
        let hints = surface.size_hints();

        if let Some(max) = max {
            if max.w > 0 {
                size.w = size.w.min(max.w);
            }
            if max.h > 0 {
                size.h = size.h.min(max.h);
            }
        }
        if let Some(min) = min {
            size.w = size.w.max(min.w);
            size.h = size.h.max(min.h);
        }

        if let Some(hints) = hints
            && let Some((inc_w_client, inc_h_client)) = hints.size_increment
        {
            let scale = self.xwayland_client_scale(surface);
            let inc_w = ((inc_w_client as f64) / scale).round().max(1.0) as i32;
            let inc_h = ((inc_h_client as f64) / scale).round().max(1.0) as i32;
            let base = base.unwrap_or_else(|| Size::from((0, 0)));

            if inc_w > 1 && size.w >= base.w {
                size.w = base.w + ((size.w - base.w) / inc_w) * inc_w;
            }
            if inc_h > 1 && size.h >= base.h {
                size.h = base.h + ((size.h - base.h) / inc_h) * inc_h;
            }
        }

        if let Some(min) = min {
            size.w = size.w.max(min.w);
            size.h = size.h.max(min.h);
        }

        size
    }
}

impl<BackendData: Backend + 'static> Xfwl4Core<BackendData> {
    pub(in crate::core::shell) fn x11_ping_window(&self, window: &WindowElement, surface: &X11Surface) {
        let ping_pending = window.x11_props().map(|props| props.ping_timeout_token.is_some()).unwrap_or(false);

        if !ping_pending {
            match surface.send_ping(self.now().as_millis()) {
                Err(PingError::NotSupported | PingError::InvalidTimestamp | PingError::PingAlreadyPending(_)) => (),
                Err(PingError::Connection(err)) => tracing::info!("Failed to send ping to X11 window 0x{:08x}: {err}", surface.window_id()),
                Ok(_) => {
                    if let Some(mut props) = window.x11_props() {
                        if let Some(token) = props.ping_timeout_token.take() {
                            self.loop_handle.remove(token);
                        }

                        let window = window.clone();
                        let token = self
                            .loop_handle
                            .insert_source(Timer::from_duration(WINDOW_PING_TIMEOUT), move |_, _, state| {
                                state.core.show_window_unresponsive_dialog(&window);
                                TimeoutAction::Drop
                            })
                            .ok();
                        props.ping_timeout_token = token;
                    }
                }
            }
        }
    }

    pub(super) fn x11_kill_client_by_surface(&self, surface: &X11Surface) {
        if let Some(xw) = self.xwayland_state.x11() {
            if xw
                .get_wm_client_machine(surface.window_id())
                .is_some_and(|client_machine| client_machine.as_c_str() == system::uname().nodename())
                && let Ok(client_pid) = surface.get_client_pid().map(|pid| pid as process::RawPid)
                && client_pid > 0
                && let Some(client_pid) = process::Pid::from_raw(client_pid)
            {
                let _ = process::kill_process(client_pid, process::Signal::KILL);
            }

            xw.kill_client_by_window(surface.window_id());
        }
    }

    pub(super) fn x11_clear_ping_timeout(&self, surface: &X11Surface) {
        if let Some(token) = surface
            .user_data()
            .get::<X11WindowProps>()
            .and_then(|props| props.0.lock().unwrap().ping_timeout_token.take())
        {
            self.loop_handle.remove(token);
        }
    }
}

impl WindowElement {
    pub(in crate::core) fn x11_props(&self) -> Option<MutexGuard<'_, X11WindowPropsInner>> {
        self.0
            .x11_surface()
            .map(|surface| surface.user_data().get_or_insert(X11WindowProps::default))
            .map(|x11_props| x11_props.0.lock().unwrap())
    }

    /// Given a rect in visible-content coordinates, return the rect in X11-window coordinates
    /// by shifting the origin outward and growing the size by the window's frame extents
    /// (drop-shadow widths reported via `_GTK_FRAME_EXTENTS`). No-op for non-X11 windows, and for
    /// X11 windows without any frame extents set.
    pub(in crate::core) fn grow_rect_by_gtk_frame_extents(&self, rect: Rectangle<i32, Logical>) -> Rectangle<i32, Logical> {
        if let Some(surface) = self.0.x11_surface() {
            rect + surface.frame_extents()
        } else {
            rect
        }
    }
}

// Motif's "all" bit inverts the meaning of the others: with it set, the bits that are listed are
// the ones *not* wanted.  Complementing normalizes that away -- and drops the "all" bit along with
// it -- leaving a plain bitfield where a present bit means the window wants that thing.
fn mwm_requested<F: bitflags::Flags + std::ops::Not<Output = F> + Copy>(hint: F, all: F) -> F {
    if hint.contains(all) { !hint } else { hint }
}

// Motif can only take capabilities away; whether the window is capable in the first place has
// already been decided by the time this is applied.
fn motif_restrict<F: bitflags::Flags + std::ops::Not<Output = F> + Copy, const N: usize>(
    hint: Option<F>,
    all: F,
    mapping: [(F, WindowCapabilities); N],
    mut capabilities: WindowCapabilities,
) -> WindowCapabilities {
    if let Some(hint) = hint {
        let requested = mwm_requested(hint, all);
        for (bit, capability) in mapping {
            capabilities.set(capability, capabilities.contains(capability) && requested.contains(bit));
        }
    }
    capabilities
}

/// Try to find a sensible content size for a newly-mapped X11 window.  smithay's
/// `SpaceElement::geometry()` returns the visible-content rect (the bounding box with
/// any frame extents already subtracted, matching the Wayland path's convention), but
/// can be 0x0 for clients that rely on the WM to size them; fall through ICCCM size
/// hints (`base_size`, `min_size`) before defaulting.
pub(in crate::core) fn x11_window_content_size(surface: &X11Surface) -> Size<i32, Logical> {
    let geometry = SpaceElement::geometry(surface);
    if geometry.size.w > 0 && geometry.size.h > 0 {
        geometry.size
    } else if let Some(base) = surface.base_size().filter(|s| s.w > 0 && s.h > 0) {
        base
    } else if let Some(min) = surface.min_size().filter(|s| s.w > 0 && s.h > 0) {
        min
    } else {
        Size::from((100, 100))
    }
}

pub(in crate::core) fn x11_motif_restrict_capabilities(surface: &X11Surface, capabilities: WindowCapabilities) -> WindowCapabilities {
    motif_restrict(
        surface.motif_hints().functions,
        MwmFunctionsHint::ALL,
        [
            (MwmFunctionsHint::CLOSE, WindowCapabilities::CLOSE),
            (MwmFunctionsHint::MINIMIZE, WindowCapabilities::MINIMIZE),
            (MwmFunctionsHint::MAXIMIZE, WindowCapabilities::MAXIMIZE),
            (MwmFunctionsHint::MOVE, WindowCapabilities::MOVE),
            (MwmFunctionsHint::RESIZE, WindowCapabilities::RESIZE),
        ],
        capabilities,
    )
}

pub(in crate::core) fn x11_motif_restrict_buttons(surface: &X11Surface, buttons: WindowCapabilities) -> WindowCapabilities {
    motif_restrict(
        surface.motif_hints().decorations,
        MwmDecorationsHint::ALL,
        [
            (MwmDecorationsHint::MENU, WindowCapabilities::WINDOW_MENU),
            (MwmDecorationsHint::MINIMIZE, WindowCapabilities::MINIMIZE),
            (MwmDecorationsHint::MAXIMIZE, WindowCapabilities::MAXIMIZE),
        ],
        buttons,
    )
}

// Motif's title and border bits are treated as a single request for a frame; drawing only one of
// the two is not supported.  A window that asks for no frame parts at all is decorating itself.
pub(in crate::core) fn x11_motif_wants_decorations(surface: &X11Surface) -> bool {
    surface.motif_hints().decorations.is_none_or(|decorations| {
        mwm_requested(decorations, MwmDecorationsHint::ALL).intersects(MwmDecorationsHint::TITLE | MwmDecorationsHint::BORDER)
    })
}

pub(in crate::core) fn x11_is_real_toplevel(surface: &X11Surface) -> bool {
    !matches!(
        surface.window_type().unwrap_or(WmWindowType::Normal),
        WmWindowType::Desktop
            | WmWindowType::Dock
            | WmWindowType::Splash
            | WmWindowType::Toolbar
            | WmWindowType::Tooltip
            | WmWindowType::Combo
            | WmWindowType::DropdownMenu
            | WmWindowType::Menu
            | WmWindowType::PopupMenu
            | WmWindowType::Notification
            | WmWindowType::Dnd
    )
}

// Motif predates _NET_WM_STATE_MODAL, and its three modal input modes all mean at least "this
// window blocks the ones it belongs with".  The widest of them, full-application modal, reaches
// further than the window tree this ends up being applied to.
pub(in crate::core) fn x11_motif_is_modal(surface: &X11Surface) -> bool {
    !matches!(surface.motif_hints().input_mode, None | Some(MwmInputMode::Modeless))
}

pub(in crate::core) fn x11_is_regular_focusable(surface: &X11Surface) -> bool {
    matches!(
        surface.window_type().unwrap_or(WmWindowType::Normal),
        WmWindowType::Normal | WmWindowType::Dialog | WmWindowType::Utility
    )
}
