use std::cell::RefCell;
use std::ops::{Deref, DerefMut};
use std::sync::Mutex;
use std::time::Instant;
use std::{cmp, mem};

use _presentation_time::wp_presentation_feedback::Kind as FeedbackKind;
use forge_ipc::{AppIdMatcher, WindowScale};
use smithay::backend::drm::{DrmEventMetadata, DrmEventTime};
use smithay::backend::renderer::element::{
    Element, Id, RenderElementPresentationState, RenderElementStates,
};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::ImportAll;
use smithay::reexports::wayland_protocols::wp::presentation_time::server as _presentation_time;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_positioner::{
    self, ConstraintAdjustment, Gravity,
};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::Resource;
use smithay::utils::{Logical, Physical, Point, Rectangle, Size, Transform};
use smithay::wayland::compositor::{
    self, SubsurfaceCachedState, SurfaceAttributes, SurfaceData, TraversalAction,
};
use smithay::wayland::foreign_toplevel_list::ForeignToplevelHandle;
use smithay::wayland::presentation::{
    PresentationFeedbackCachedState, PresentationFeedbackCallback, Refresh,
};
use smithay::wayland::shell::wlr_layer::{
    Anchor, ExclusiveZone, KeyboardInteractivity, Layer, LayerSurfaceCachedState,
};
use smithay::wayland::shell::xdg::{
    PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceRoleAttributes,
    XdgToplevelSurfaceRoleAttributes,
};
use smithay::wayland::{fractional_scale, single_pixel_buffer};
use tracing::error;

use crate::drawing::{ForgeElement, ForgeSurfaceData, RenderTexture, Texture};
use crate::geometry::Vector;
use crate::output::{ExclusiveSpace, Output};
use crate::windows;
use crate::windows::surface::{ForgeLayerSurface, InputSurface, Surface};

#[derive(Debug)]
pub struct Window<S = ToplevelSurface> {
    pub acked_size: Size<i32, Logical>,
    pub deny_focus: bool,
    pub surface: S,
    pub app_id: Option<String>,
    pub urgent: bool,

    /// Desktop-mode flag: when true the window is not subject to tiling resize.
    /// The window manages its own rectangle via `set_floating_position` /
    /// the regular `set_dimensions` path for explicit size changes.
    pub floating: bool,

    /// When `Some`, the window is currently maximized; the stored rectangle
    /// is the previous floating rect used on un-maximize.
    pre_max_rect: Option<Rectangle<i32, Logical>>,

    /// When `Some`, the window is currently fullscreened; the stored rectangle
    /// is the previous (floating) rect to be restored on un-fullscreen. Tiled
    /// windows do not need this — `Layout::resize` re-tiles them — but
    /// floating windows would otherwise keep the fullscreen rectangle (which
    /// includes the topbar strip) and end up covering the topbar after
    /// leaving fullscreen.
    pre_fullscreen_rect: Option<Rectangle<i32, Logical>>,

    /// When true, the window is minimized: it is not rendered and does not
    /// receive pointer/touch input. It stays in Alt+Tab / overview so the
    /// user can restore it.
    pub minimized: bool,

    /// Client explicitly asked for client-side decorations via
    /// zxdg_toplevel_decoration_v1.request_mode(ClientSide). We skip
    /// drawing our SSD for these windows so the client's own title bar
    /// is the only one visible.
    pub wants_csd: bool,

    dirty: bool,
    output_rectangle: Rectangle<i32, Logical>,
    size: Size<i32, Logical>,
    texture_cache: TextureCache,
    /// Last topmost texture that successfully landed in `texture_cache` —
    /// kept around so the close animation has something to fade out even
    /// when the client commits a `wl_surface.attach(NULL)` (or otherwise
    /// drops its only texture) right before `toplevel_destroyed`. Foot
    /// terminal hits this path on quit: a NULL-attach commit empties the
    /// cache, the destroy fires, `thumbnail_texture` would have nothing to
    /// return and the close animation would be silently skipped.
    last_good_thumbnail: Option<RenderTexture>,
    visible: bool,
    transaction: Option<WindowTransaction>,
    popups: Vec<Window<PopupSurface>>,
    presentation_callbacks: Vec<PresentationCallback>,
    scale: Option<WindowScale>,
    ignore_transactions: bool,
    ignore_transactions_locked: bool,
    dead: bool,

    /// `ext-foreign-toplevel-list-v1` handle. `None` on popups and
    /// layer surfaces. Drop sends the `closed` event automatically.
    pub foreign_handle: Option<ForeignToplevelHandle>,

    /// Set in `Windows::add` for floating desktop windows. The first
    /// commit that brings a non-empty `app_id` checks the geometry
    /// store, applies any saved rect, and clears the flag. Tiled and
    /// mobile windows are not seeded — their geometry is dictated by
    /// the layout, not the client's last session.
    pub needs_geometry_restore: bool,
}

impl<S: Surface + 'static> Window<S> {
    pub fn new(
        surface: S,
        output_scale: f64,
        surface_transform: Transform,
        app_id: Option<String>,
    ) -> Self {
        let window = Self {
            surface,
            app_id,
            ignore_transactions_locked: false,
            presentation_callbacks: Vec::new(),
            ignore_transactions: false,
            output_rectangle: Rectangle::default(),
            texture_cache: TextureCache::default(),
            last_good_thumbnail: None,
            transaction: None,
            deny_focus: false,
            acked_size: Size::default(),
            visible: false,
            popups: Vec::new(),
            urgent: false,
            dirty: false,
            scale: None,
            size: Size::default(),
            dead: false,
            floating: false,
            pre_max_rect: None,
            pre_fullscreen_rect: None,
            minimized: false,
            wants_csd: false,
            foreign_handle: None,
            needs_geometry_restore: false,
        };

        window.update_scale(output_scale);
        window.update_transform(surface_transform);

        window
    }

    pub fn request_frame(&self, runtime: u32) {
        self.with_surfaces(|_, data| {
            let mut attrs = data.cached_state.get::<SurfaceAttributes>();
            for cb in attrs.current().frame_callbacks.drain(..) {
                cb.done(runtime);
            }
        });

        for popup in &self.popups {
            popup.request_frame(runtime);
        }
    }

    pub fn alive(&self) -> bool {
        !self.dead && self.surface.alive()
    }

    pub fn textures(
        &self,
        textures: &mut Vec<ForgeElement>,
        output_scale: f64,
        window_scale: impl Into<Option<f64>>,
        location: impl Into<Option<Point<i32, Logical>>>,
    ) {
        self.textures_with_bounds(textures, output_scale, window_scale, location, None, None);
    }

    /// Like [`textures`] but rounds the bottom corners of the window body,
    /// used by the desktop SSD paint path so a server-decorated window's
    /// bottom matches the rounded top of its title bar. `round` is `None`
    /// for windows forge does not decorate.
    pub fn textures_rounded(
        &self,
        textures: &mut Vec<ForgeElement>,
        output_scale: f64,
        round: Option<&crate::rounded::Rounding>,
    ) {
        self.textures_with_bounds(textures, output_scale, None, None, None, round);
    }

    /// Return the topmost rendered texture for this window, used by Alt+Tab
    /// thumbnails and the close-animation tombstone. Falls back to the
    /// last-known-good thumbnail when the live cache is empty — this happens
    /// for clients (foot terminal, …) that commit a NULL buffer right before
    /// destroying the toplevel: the empty commit clears `texture_cache` and
    /// without this fallback the close animation has nothing to fade out.
    /// Still returns `None` for windows that have never rendered a frame.
    pub fn thumbnail_texture(&self) -> Option<RenderTexture> {
        self.pick_thumbnail_texture()
            .or_else(|| self.last_good_thumbnail.clone())
    }

    /// Pixel size of the thumbnail texture in physical pixels (or `None`).
    /// Mirrors [`thumbnail_texture`]'s picking rule so callers can size the
    /// thumbnail to the actual window content's aspect ratio, not a stray
    /// subsurface.
    pub fn thumbnail_size(&self) -> Option<smithay::utils::Size<i32, smithay::utils::Logical>> {
        self.pick_thumbnail_texture()
            .map(|t| t.size())
            .or_else(|| self.last_good_thumbnail.as_ref().map(|t| t.size()))
    }

    /// Pick the texture that best represents the window's visible content.
    /// Apps with CSD drop-shadows (Firefox, GTK4) commit a sub-surface that
    /// is larger than the content but mostly transparent — a plain
    /// max-by-area pick yields that shadow buffer and the thumbnail reads
    /// as empty. Prefer the texture whose size matches the xdg-shell
    /// `set_window_geometry` rect; fall back to max-by-area when the
    /// window does not declare a geometry.
    fn pick_thumbnail_texture(&self) -> Option<RenderTexture> {
        if let Some(geom) = self.texture_cache.geometry_size {
            if let Some(t) = self.texture_cache.textures.iter().min_by_key(|t| {
                let s = t.size();
                ((s.w - geom.w).abs() + (s.h - geom.h).abs()) as i64
            }) {
                return Some(t.clone());
            }
        }
        self.texture_cache
            .textures
            .iter()
            .max_by_key(|t| {
                let s = t.size();
                (s.w as i64) * (s.h as i64)
            })
            .cloned()
    }

    pub fn textures_with_bounds(
        &self,
        textures: &mut Vec<ForgeElement>,
        output_scale: f64,
        window_scale: impl Into<Option<f64>>,
        location: impl Into<Option<Point<i32, Logical>>>,
        bounds: impl Into<Option<Rectangle<i32, Physical>>>,
        round: Option<&crate::rounded::Rounding>,
    ) {
        let location = location
            .into()
            .unwrap_or_else(|| self.bounds(output_scale).loc);
        let window_scale = window_scale.into().unwrap_or(1.0);

        let bounds = bounds.into().unwrap_or_else(|| {
            let mut b = self
                .bounds(output_scale)
                .to_physical_precise_round(output_scale);
            b.loc = location.to_physical_precise_round(output_scale);
            b
        });

        for popup in self.popups.iter().rev() {
            let popup_loc = location + popup.bounds(output_scale).loc.scale(window_scale);
            popup.textures_with_bounds(
                textures,
                output_scale,
                window_scale,
                Some(popup_loc),
                bounds,
                None,
            );
        }

        // Round only the texture that represents the window body — the one
        // covering the bottom corners — never a stray subsurface (CSD
        // shadow, popup overlay).
        let round_id: Option<Id> = round
            .and_then(|_| self.pick_thumbnail_texture())
            .map(|t| t.id().clone());

        // Re-apply the origin shift that `TextureCache::reset_origin` removed
        // when normalising negative subsurface offsets to a (0,0)-based space.
        // Without this a subsurface positioned above the parent origin (e.g. a
        // slide-up reveal) stays pinned at the origin instead of moving. Matches
        // the input path, which subtracts `origin_delta` (see `surface_under`).
        let physical_loc = ( location + self.texture_cache.origin_delta ).to_physical_precise_round(output_scale);
        for texture in self.texture_cache.textures.iter().rev() {
            if let Some(r) = round {
                if Some(texture.id()) == round_id.as_ref() {
                    ForgeElement::add_element_rounded(
                        textures,
                        texture.clone(),
                        physical_loc,
                        bounds,
                        window_scale,
                        output_scale,
                        r.program.clone(),
                        r.radius,
                    );
                    continue;
                }
            }
            ForgeElement::add_element(
                textures,
                texture.clone(),
                physical_loc,
                bounds,
                window_scale,
                output_scale,
            );
        }
    }

    pub fn bounds(&self, output_scale: f64) -> Rectangle<i32, Logical> {
        let mut bounds = self.output_rectangle;
        bounds.loc += self.internal_offset(output_scale);
        bounds
    }

    /// The compositor-tracked position and size of this window, independent
    /// of texture state. Unlike [`bounds`], this does NOT add an internal
    /// offset derived from the texture cache — safe to call even when the
    /// cache is empty (e.g., after a null-buffer commit).
    pub fn allocated_rect(&self) -> Rectangle<i32, Logical> {
        self.output_rectangle
    }

    pub fn internal_offset(&self, output_scale: f64) -> Point<i32, Logical> {
        let cache_size = self.texture_cache.size();
        let is_mobile = output_scale >= 2.0;
        let offset_x = if is_mobile {
            ((self.size.w - cache_size.w) / 2).max(0)
        } else {
            0
        };
        let offset_y = ((self.size.h - cache_size.h) / 2).max(0);
        let window_offset = Point::from((offset_x, offset_y));

        let scale = self.window_scale(output_scale);
        window_offset.scale(scale / output_scale)
    }

    pub fn import_buffers(&mut self, renderer: &mut GlesRenderer) {
        if self.transaction.is_some() {
            return;
        }

        for popup in &mut self.popups {
            popup.import_buffers(renderer);
        }

        if !mem::take(&mut self.dirty) {
            return;
        }

        let geometry = self.surface.geometry();
        self.texture_cache
            .reset(self.size, geometry.map(|g| g.size));

        compositor::with_surface_tree_upward(
            self.surface.surface(),
            Point::from((0, 0)) - geometry.unwrap_or_default().loc,
            |_, surface_data, location| {
                let Some(data) = surface_data.data_map.get::<RefCell<ForgeSurfaceData>>() else {
                    return TraversalAction::SkipChildren;
                };
                let mut data = data.borrow_mut();

                let mut location = *location;
                if surface_data.role == Some("subsurface") {
                    let mut subsurface = surface_data.cached_state.get::<SubsurfaceCachedState>();
                    location += subsurface.current().location;
                }

                data.location = location;

                TraversalAction::DoChildren(location)
            },
            |surface, surface_data, _| {
                let Some(data) = surface_data.data_map.get::<RefCell<ForgeSurfaceData>>() else {
                    return;
                };
                let mut data = data.borrow_mut();

                if let Some(texture) = &data.texture {
                    texture.set_location(data.location);
                    self.texture_cache.push(texture.clone(), data.location);
                    return;
                }

                let Some(buffer) = &data.buffer else { return };

                let mut feedback = surface_data
                    .cached_state
                    .get::<PresentationFeedbackCachedState>();
                for cb in feedback.current().callbacks.drain(..) {
                    self.presentation_callbacks
                        .push(PresentationCallback::new(surface.clone(), cb));
                }

                let damage = data.damage.buffer();

                let texture = match single_pixel_buffer::get_single_pixel_buffer(buffer) {
                    Ok(spb) => {
                        Texture::from_spb(spb.rgba8888(), self.scale, data.location, &data, surface)
                    }
                    Err(_) => match renderer.import_buffer(buffer, Some(surface_data), damage) {
                        Some(Ok(tex)) => {
                            data.buffer = None;
                            Texture::from_surface(tex, self.scale, data.location, &data, surface)
                        }
                        _ => {
                            error!("unable to import buffer");
                            data.buffer = None;
                            return;
                        }
                    },
                };

                let render_texture = RenderTexture::new(texture);
                self.texture_cache
                    .push(render_texture.clone(), data.location);
                data.texture = Some(render_texture);

                data.damage.clear();
            },
            |_, _, _| true,
        );

        if geometry.is_none() {
            self.texture_cache.reset_origin();
        }

        // Snapshot the freshly populated topmost texture for the close
        // animation's fallback path. Skipped on empty rebuilds (the client
        // committed a NULL buffer) — keeping the previous value is the
        // whole point of the fallback. Route through the same picker as
        // [`thumbnail_texture`] so a CSD-shadow subsurface doesn't become
        // the snapshot for Firefox / GTK4.
        if let Some(top) = self.pick_thumbnail_texture() {
            self.last_good_thumbnail = Some(top);
        }
    }

    pub fn mark_presented(
        &mut self,
        states: &RenderElementStates,
        metadata: &Option<DrmEventMetadata>,
        output: &Output,
        start_time: &Instant,
    ) {
        if self.transaction.is_some() {
            return;
        }

        let refresh = Refresh::Fixed(output.frame_interval());
        let output = output.smithay_output();

        let time = metadata.as_ref().and_then(|m| match m.time {
            DrmEventTime::Monotonic(t) => Some(t),
            DrmEventTime::Realtime(_) => None,
        });
        let seq = metadata.as_ref().map(|m| m.sequence).unwrap_or(0);

        let (time, flags) = match time {
            Some(t) => (
                t,
                FeedbackKind::Vsync | FeedbackKind::HwClock | FeedbackKind::HwCompletion,
            ),
            None => (start_time.elapsed(), FeedbackKind::Vsync),
        };

        for PresentationCallback { callback, surface } in self.presentation_callbacks.drain(..) {
            let zero_copy = states
                .element_render_state(&surface)
                .is_some_and(|s| s.presentation_state == RenderElementPresentationState::ZeroCopy);
            let flags = if zero_copy {
                FeedbackKind::ZeroCopy
            } else {
                flags
            };

            callback.presented(output, time, refresh, seq as u64, flags);
        }
    }

    pub fn set_dimensions(&mut self, output_scale: f64, rectangle: Rectangle<i32, Logical>) {
        let mut size = rectangle.size;
        if let Some(window_scale) = &self.scale {
            let ws = window_scale.scale(output_scale);
            size = size.scale(output_scale / ws);
        }

        let current = (self.output_rectangle, self.size);
        let transaction_current = self
            .transaction
            .as_ref()
            .map(|t| (t.output_rectangle, t.size));
        if (rectangle, size) == transaction_current.unwrap_or(current) {
            return;
        }

        let transaction = self.start_transaction();
        transaction.output_rectangle = rectangle;
        transaction.size = size;

        self.surface.resize(size);
    }

    pub fn set_dimensions_interactive(
        &mut self,
        output_scale: f64,
        rectangle: Rectangle<i32, Logical>,
    ) {
        let mut size = rectangle.size;
        if let Some(window_scale) = &self.scale {
            let ws = window_scale.scale(output_scale);
            size = size.scale(output_scale / ws);
        }

        if rectangle == self.output_rectangle && size == self.size {
            return;
        }

        self.transaction = None;
        self.output_rectangle = rectangle;
        self.size = size;
        self.surface.resize(size);
    }

    /// Move a floating window without changing its size.
    ///
    /// Unlike `set_dimensions`, this does not go through the configure/ack
    /// transaction path — size is unchanged, so no client-side reconfigure
    /// is needed. Used by desktop-mode title-bar drag.
    pub fn set_floating_position(&mut self, loc: Point<i32, Logical>) {
        self.output_rectangle.loc = loc;
        if let Some(transaction) = self.transaction.as_mut() {
            transaction.output_rectangle.loc = loc;
        }
    }

    /// Current window rectangle on the output (pre-internal-offset).
    pub fn output_rectangle(&self) -> Rectangle<i32, Logical> {
        self.output_rectangle
    }

    pub fn enter(&mut self, output: &Output) {
        self.with_surfaces(|s, _| output.enter(s));
        self.visible = true;
    }

    pub fn leave(&mut self, output: &Output) {
        self.with_surfaces(|s, _| output.leave(s));
        self.visible = false;
    }

    fn with_surfaces<F: FnMut(&WlSurface, &SurfaceData)>(&self, mut fun: F) {
        compositor::with_surface_tree_upward(
            self.surface.surface(),
            (),
            |_, _, _| TraversalAction::DoChildren(()),
            |surface, data, _| fun(surface, data),
            |_, _, _| true,
        );
    }

    fn start_transaction(&mut self) -> &mut WindowTransaction {
        windows::start_transaction();
        self.transaction.get_or_insert(WindowTransaction::new(self))
    }

    pub fn apply_transaction(&mut self) {
        let Some(transaction) = self.transaction.take() else {
            return;
        };

        if self.ignore_transactions && !self.ignore_transactions_locked {
            self.ignore_transactions = transaction.size != self.acked_size;

            if self.ignore_transactions {
                self.ignore_transactions_locked = true;
            }
        }

        self.output_rectangle = transaction.output_rectangle;
        self.size = transaction.size;
    }

    pub fn transaction_done(&self) -> bool {
        self.ignore_transactions
            || !self.alive()
            || self
                .transaction
                .as_ref()
                .is_none_or(|t| t.size == self.acked_size)
    }

    pub fn surface_commit_common(
        &mut self,
        output_scale: f64,
        window_scales: &[(AppIdMatcher, WindowScale)],
        surface: &WlSurface,
    ) {
        self.acked_size = self.surface.acked_size();
        self.ignore_transactions_locked = false;

        // Floating desktop windows spawn with position but no size, so
        // the initial configure carries `size = None` (xdg-shell client-
        // picks-its-own-size). On the first commit we adopt whatever the
        // client negotiated via `set_window_geometry`; if it produced a
        // tiny buffer (Firefox / Chromium / GTK toolkits ignore size=None
        // and fall back to ~64x64), we re-configure with a 1024x720
        // baseline so the user sees a usable window.
        if self.floating && self.output_rectangle.size.w == 0 && self.output_rectangle.size.h == 0 {
            const MIN_REASONABLE: i32 = 320;
            const FALLBACK_W: i32 = 1024;
            const FALLBACK_H: i32 = 720;
            if self.acked_size.w >= MIN_REASONABLE && self.acked_size.h >= MIN_REASONABLE {
                self.output_rectangle.size = self.acked_size;
                self.size = self.acked_size;
            } else if self.acked_size.w > 0 || self.acked_size.h > 0 {
                let fallback = Size::<i32, Logical>::from((FALLBACK_W, FALLBACK_H));
                self.output_rectangle.size = fallback;
                self.size = fallback;
                self.surface.resize(fallback);
            }
        }

        compositor::with_surface_tree_upward(
            surface,
            (),
            |_, data, _| {
                self.dirty = true;

                let mut surface_data = data
                    .data_map
                    .get_or_insert(|| RefCell::new(ForgeSurfaceData::new()))
                    .borrow_mut();

                let mut attrs = data.cached_state.get::<SurfaceAttributes>();
                let attrs = attrs.current();
                let Some(buffer_assignment) = attrs.buffer.take() else {
                    return TraversalAction::DoChildren(());
                };

                surface_data.opaque_region = attrs
                    .opaque_region
                    .as_mut()
                    .map(|r| mem::take(&mut r.rects))
                    .unwrap_or_default();

                surface_data.update_buffer(data, attrs, buffer_assignment);

                TraversalAction::DoChildren(())
            },
            |_, _, _| (),
            |_, _, _| true,
        );

        self.surface.initial_configure();

        let app_id_changed = compositor::with_states(surface, |states| {
            let attrs = states
                .data_map
                .get::<Mutex<XdgToplevelSurfaceRoleAttributes>>()
                .and_then(|a| a.lock().ok());

            match attrs.filter(|a| a.app_id != self.app_id) {
                Some(a) => {
                    self.set_app_id(a.app_id.clone());
                    true
                }
                None => false,
            }
        });

        if app_id_changed {
            if let Some(ref handle) = self.foreign_handle {
                handle.send_app_id(self.app_id.as_deref().unwrap_or(""));
                handle.send_done();
            }
            self.set_window_scale(window_scales, output_scale);
        }
    }

    pub fn update_transform(&self, surface_transform: Transform) {
        for popup in &self.popups {
            popup.update_transform(surface_transform);
        }

        let surface = self.surface.surface();
        if surface.version() >= 6 {
            surface.preferred_buffer_transform(surface_transform.into());
        }
    }

    pub fn set_window_scale(
        &mut self,
        window_scales: &[(AppIdMatcher, WindowScale)],
        output_scale: f64,
    ) {
        let window_scale = window_scales
            .iter()
            .find_map(|(matcher, ws)| matcher.matches(self.app_id.as_ref()).then_some(*ws));
        if window_scale == self.scale {
            return;
        }
        self.set_window_scale_internal(window_scale);

        self.update_scale(output_scale);

        let current_rect = self
            .transaction
            .as_ref()
            .map_or(self.output_rectangle, |t| t.output_rectangle);
        self.set_dimensions(output_scale, current_rect);
    }

    fn set_window_scale_internal(&mut self, window_scale: Option<WindowScale>) {
        for popup in &mut self.popups {
            popup.set_window_scale_internal(window_scale);
        }
        self.scale = window_scale;
    }

    pub fn update_scale(&self, output_scale: f64) {
        for popup in &self.popups {
            popup.update_scale(output_scale);
        }

        self.set_preferred_scale_internal(output_scale);
    }

    fn set_preferred_scale_internal(&self, output_scale: f64) {
        let scale = self.window_scale(output_scale);
        let surface = self.surface.surface();

        compositor::with_states(surface, |states| {
            let data = states
                .data_map
                .get_or_insert(|| RefCell::new(ForgeSurfaceData::new()));
            data.borrow_mut().preferred_fractional_scale = scale;

            fractional_scale::with_fractional_scale(states, |fs| {
                fs.set_preferred_scale(scale);
            });
        });

        if surface.version() >= 6 {
            surface.preferred_buffer_scale(scale.ceil() as i32);
        }
    }

    fn window_scale(&self, output_scale: f64) -> f64 {
        self.scale.map_or(output_scale, |s| s.scale(output_scale))
    }

    pub fn surface_at(
        &self,
        output_scale: f64,
        position: Point<f64, Logical>,
    ) -> Option<InputSurface> {
        self.surface_at_internal(output_scale, position, Point::default())
    }

    fn surface_at_internal(
        &self,
        output_scale: f64,
        mut position: Point<f64, Logical>,
        popup_offset: Point<f64, Logical>,
    ) -> Option<InputSurface> {
        let window_scale = self.window_scale(output_scale);
        let bounds = self.bounds(output_scale);
        let bounds_loc = bounds.loc.scale(output_scale / window_scale).to_f64() + popup_offset;

        let popup = self
            .popups
            .iter()
            .find_map(|p| p.surface_at_internal(output_scale, position, bounds_loc.to_f64()));
        if popup.is_some() {
            return popup;
        }

        position = position.upscale(output_scale / window_scale);

        let geometry = self.surface.geometry().unwrap_or_default();
        let origin_delta = self.texture_cache.origin_delta;

        let result = RefCell::new(None);
        compositor::with_surface_tree_downward(
            self.surface.surface(),
            bounds_loc,
            |_, surface_data, location| {
                if result.borrow().is_some() {
                    return TraversalAction::SkipChildren;
                }

                let mut location = *location;
                if surface_data.role == Some("subsurface") {
                    let mut cached = surface_data.cached_state.get::<SubsurfaceCachedState>();
                    location += cached.current().location.to_f64();
                }

                TraversalAction::DoChildren(location)
            },
            |wl_surface, surface_data, location| {
                if result.borrow().is_some() {
                    return;
                }

                let mut location = *location;
                if surface_data.role == Some("subsurface") {
                    let mut cached = surface_data.cached_state.get::<SubsurfaceCachedState>();
                    location += cached.current().location.to_f64();
                }

                let size = surface_data
                    .data_map
                    .get::<RefCell<ForgeSurfaceData>>()
                    .map_or_else(Size::default, |d| d.borrow().dst_size);
                let surface_loc = location - geometry.loc.to_f64() - origin_delta.to_f64();
                let surface_rect = Rectangle::new(surface_loc, size.to_f64());

                let mut attrs = surface_data.cached_state.get::<SurfaceAttributes>();
                let input_region = attrs.current().input_region.as_ref();
                let input_position = (position - surface_rect.loc).to_i32_round();

                if surface_rect.contains(position)
                    && input_region.is_none_or(|r| r.contains(input_position))
                {
                    let surface = InputSurface::new(wl_surface.clone(), surface_loc, window_scale);
                    *result.borrow_mut() = Some(surface);
                }
            },
            |_, _, _| result.borrow().is_none(),
        );
        result.into_inner()
    }

    pub fn owns_surface(&self, surface: &WlSurface) -> bool {
        let mut owns = false;
        self.with_surfaces(|ws, _| owns |= ws == surface);
        owns
    }

    pub fn add_popup(
        &mut self,
        mut popup: Window<PopupSurface>,
        parent: &WlSurface,
    ) -> Option<Window<PopupSurface>> {
        if self.surface.surface() == parent {
            popup.app_id.clone_from(&self.app_id);
            popup.visible = self.visible;
            popup.scale = self.scale;

            self.popups.push(popup);
            return None;
        }

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

        Some(popup)
    }

    pub fn popup_surface_commit(
        &mut self,
        output_scale: f64,
        output_size: Size<i32, Logical>,
        parent_abs_loc: Point<i32, Logical>,
        root_surface: &WlSurface,
        surface: &WlSurface,
    ) -> bool {
        let window_scale = self.window_scale(output_scale);
        let bounds = Rectangle::new(
            Point::from((-parent_abs_loc.x, -parent_abs_loc.y)),
            output_size,
        );

        self.popups.iter_mut().any(|popup| {
            if popup.surface.surface() == root_surface {
                let popup_loc = popup.constrained_location(bounds);
                popup.output_rectangle.loc = popup_loc.scale(window_scale / output_scale);

                popup.surface.with_pending_state(|state| {
                    state.geometry.loc = popup.output_rectangle.loc;
                });

                popup.surface_commit_common(output_scale, &[], surface);

                return true;
            }

            let child_parent_loc = parent_abs_loc + popup.output_rectangle.loc;
            popup.popup_surface_commit(
                output_scale,
                output_size,
                child_parent_loc,
                root_surface,
                surface,
            )
        })
    }

    pub fn refresh_popups(&mut self) {
        for i in (0..self.popups.len()).rev() {
            self.popups[i].refresh_popups();

            if !self.popups[i].alive() {
                self.popups.swap_remove(i);
            }
        }
    }

    pub fn reposition_popup(
        &mut self,
        output_scale: f64,
        output_size: Size<i32, Logical>,
        parent_abs_loc: Point<i32, Logical>,
        surface: &PopupSurface,
        token: u32,
    ) {
        let window_scale = self.window_scale(output_scale);
        let bounds = Rectangle::new(
            Point::from((-parent_abs_loc.x, -parent_abs_loc.y)),
            output_size,
        );

        for popup in &mut self.popups {
            if &popup.surface != surface {
                let child_parent_loc = parent_abs_loc + popup.output_rectangle.loc;
                popup.reposition_popup(output_scale, output_size, child_parent_loc, surface, token);
                continue;
            }

            let popup_loc = popup.constrained_location(bounds);
            popup.output_rectangle.loc = popup_loc.scale(window_scale / output_scale);

            popup.surface.with_pending_state(|state| {
                state.geometry.loc = popup.output_rectangle.loc;
            });

            surface.send_repositioned(token);
        }
    }

    pub fn set_app_id(&mut self, app_id: Option<String>) {
        for popup in &mut self.popups {
            popup.set_app_id(app_id.clone());
        }

        self.app_id = app_id;
    }

    pub fn set_ignore_transactions(&mut self) {
        self.ignore_transactions_locked = true;
        self.ignore_transactions = true;
    }

    pub fn dirty(&self) -> bool {
        self.transaction.is_none() && (self.dirty || self.popups.iter().any(|p| p.dirty()))
    }

    pub fn surface(&self) -> &WlSurface {
        self.surface.surface()
    }
}

impl Window {
    pub fn kill(&mut self) {
        self.surface.send_close();
        self.dead = true;
    }

    pub fn is_maximized(&self) -> bool {
        self.pre_max_rect.is_some()
    }

    /// The rect that should be persisted for geometry-restore purposes.
    /// For a maximized window this is the pre-maximize rect (the rect a
    /// future un-maximize would restore to), not the current full
    /// avail-minus-topbar rect — saving the latter would make the
    /// restored window start maximized *and* lose its prior floating
    /// size, which then permanently coincides with `avail`.
    pub fn persist_rect(&self) -> Rectangle<i32, Logical> {
        self.pre_max_rect.unwrap_or(self.output_rectangle)
    }

    pub fn suppress_ssd(&self, csd_apps: &[AppIdMatcher]) -> bool {
        self.wants_csd || csd_apps.iter().any(|m| m.matches(self.app_id.as_ref()))
    }

    /// Toggle the maximized state against `avail` (the output work area,
    /// already reduced by topbar and exclusive zones). The title bar lives
    /// above `output_rectangle.loc.y`, so the maximized rect is shrunk by
    /// the title bar height on top.
    pub fn toggle_maximize(
        &mut self,
        output_scale: f64,
        avail: Rectangle<i32, Logical>,
        bar_h: i32,
    ) {
        use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State;

        if let Some(restore) = self.pre_max_rect.take() {
            self.surface.with_pending_state(|state| {
                state.states.unset(State::Maximized);
            });
            self.set_dimensions(output_scale, restore);
        } else {
            let prev = self.output_rectangle;
            let max_rect = Rectangle::<i32, Logical>::new(
                (avail.loc.x, avail.loc.y + bar_h).into(),
                (avail.size.w, (avail.size.h - bar_h).max(1)).into(),
            );
            self.pre_max_rect = Some(prev);
            self.surface.with_pending_state(|state| {
                state.states.set(State::Maximized);
            });
            self.set_dimensions(output_scale, max_rect);
        }
    }

    /// Client-declared (min_size, max_size) in logical pixels. A zero
    /// component means "no constraint".
    pub fn size_constraints(&self) -> (Size<i32, Logical>, Size<i32, Logical>) {
        use smithay::wayland::shell::xdg::SurfaceCachedState;
        compositor::with_states(self.surface.surface(), |states| {
            let mut cached = states.cached_state.get::<SurfaceCachedState>();
            let cur = cached.current();
            (cur.min_size, cur.max_size)
        })
    }

    /// Un-maximize, forcing a restore even if the current rect differs.
    /// Used when a drag should implicitly "tear off" a maximized window.
    pub fn unmaximize(&mut self, output_scale: f64) {
        use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State;
        let Some(restore) = self.pre_max_rect.take() else {
            return;
        };
        self.surface.with_pending_state(|state| {
            state.states.unset(State::Maximized);
        });
        self.set_dimensions(output_scale, restore);
    }

    pub fn set_resizing(&mut self, resizing: bool) {
        use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State;
        self.surface.with_pending_state(|state| {
            if resizing {
                state.states.set(State::Resizing);
            } else {
                state.states.unset(State::Resizing);
            }
        });
        if self.surface.initial_configure_sent() {
            self.surface.send_configure();
        }
    }

    /// Snapshot the current `output_rectangle` so a later
    /// [`Self::exit_fullscreen`] can land the window back on its
    /// pre-fullscreen geometry instead of leaving it stuck at the
    /// fullscreen rectangle (which covers the topbar).
    ///
    /// Tiled windows still get this snapshot; the cost is one rect copy
    /// and `Layout::resize` will overwrite the rect on the way out anyway.
    pub fn enter_fullscreen(&mut self) {
        self.pre_fullscreen_rect = Some(self.output_rectangle);
    }

    /// Pop the saved rect and resize back to it. No-op when there is no
    /// snapshot — calling this on a window that never went fullscreen
    /// leaves the geometry untouched.
    pub fn exit_fullscreen(&mut self, output_scale: f64) {
        if let Some(restore) = self.pre_fullscreen_rect.take() {
            self.set_dimensions(output_scale, restore);
        }
    }
}

impl Window<PopupSurface> {
    pub fn parent(&self) -> Option<WlSurface> {
        compositor::with_states(self.surface.surface(), |states| {
            let attrs = states
                .data_map
                .get::<Mutex<XdgPopupSurfaceRoleAttributes>>()?;
            attrs.lock().ok()?.parent.clone()
        })
    }

    fn constrained_location(&self, bounds: Rectangle<i32, Logical>) -> Point<i32, Logical> {
        let size = self.texture_cache.size();
        self.surface.with_pending_state(|state| {
            PopupConstrainer(&mut state.positioner).constrained_location(bounds, size)
        })
    }
}

impl Window<ForgeLayerSurface> {
    pub fn surface_commit(
        &mut self,
        window_scales: &[(AppIdMatcher, WindowScale)],
        output: &mut Output,
        fullscreen_active: bool,
        surface: &WlSurface,
    ) {
        self.update_layer_state(output);
        self.update_dimensions(output, fullscreen_active);
        self.surface_commit_common(output.scale(), window_scales, surface);
    }

    pub fn update_dimensions(&mut self, output: &mut Output, fullscreen_active: bool) {
        let state = compositor::with_states(self.surface.surface(), |states| {
            *states
                .cached_state
                .get::<LayerSurfaceCachedState>()
                .current()
        });

        let output_size = match state.layer {
            Layer::Overlay if fullscreen_active => output.size(),
            _ => output.wm_size(),
        };

        let output_scale = output.scale();
        let scale = self.window_scale(output_scale);
        let mut size = state.size.scale(scale / output_scale);

        let exclusive = match state.exclusive_zone {
            ExclusiveZone::Neutral => *output.exclusive(),
            _ => ExclusiveSpace::default(),
        };

        if size.w == 0 && state.anchor.contains(Anchor::LEFT | Anchor::RIGHT) {
            size.w = output_size.w - exclusive.left - exclusive.right;
            if state.anchor.contains(Anchor::RIGHT) {
                size.w -= state.margin.right;
            }
        }
        if size.h == 0 && state.anchor.contains(Anchor::TOP | Anchor::BOTTOM) {
            size.h = output_size.h - exclusive.top - exclusive.bottom;
            if state.anchor.contains(Anchor::BOTTOM) {
                size.h -= state.margin.bottom;
            }
        }

        let x = if state.anchor.contains(Anchor::LEFT) {
            state.margin.left + exclusive.left
        } else if state.anchor.contains(Anchor::RIGHT) {
            output_size.w - size.w - state.margin.right - exclusive.right
        } else {
            (output_size.w - size.w) / 2
        };
        let y = if state.anchor.contains(Anchor::TOP) {
            state.margin.top + exclusive.top
        } else if state.anchor.contains(Anchor::BOTTOM) {
            output_size.h - size.h - state.margin.bottom - exclusive.bottom
        } else {
            (output_size.h - size.h) / 2
        };

        self.set_dimensions(output.scale(), Rectangle::new((x, y).into(), size));
    }

    fn update_layer_state(&mut self, output: &mut Output) {
        let state = compositor::with_states(self.surface.surface(), |states| {
            *states
                .cached_state
                .get::<LayerSurfaceCachedState>()
                .current()
        });

        self.deny_focus = state.keyboard_interactivity == KeyboardInteractivity::None;

        let exclusive_zone = match state.exclusive_zone {
            ExclusiveZone::Exclusive(size) => {
                let output_scale = output.scale();
                let scale = self.window_scale(output_scale);
                ExclusiveZone::Exclusive((size as f64 * (scale / output_scale)).round() as u32)
            }
            ez => ez,
        };

        let old_exclusive = mem::replace(&mut self.surface.exclusive_zone, exclusive_zone);
        let old_anchor = mem::replace(&mut self.surface.anchor, state.anchor);
        output.exclusive().reset(old_anchor, old_exclusive);
        output
            .exclusive()
            .update(state.anchor, exclusive_zone, state.layer);
    }
}

#[derive(Debug)]
struct WindowTransaction {
    output_rectangle: Rectangle<i32, Logical>,
    size: Size<i32, Logical>,
}

impl WindowTransaction {
    fn new<S>(current: &Window<S>) -> Self {
        Self {
            output_rectangle: current.output_rectangle,
            size: current.size,
        }
    }
}

#[derive(Default, Debug)]
pub struct TextureCache {
    geometry_size: Option<Size<i32, Logical>>,
    texture_size: Size<i32, Logical>,
    requested_size: Size<i32, Logical>,
    origin: Option<Point<i32, Logical>>,
    origin_delta: Point<i32, Logical>,
    textures: Vec<RenderTexture>,
}

impl TextureCache {
    fn reset(
        &mut self,
        requested_size: Size<i32, Logical>,
        geometry_size: Option<Size<i32, Logical>>,
    ) {
        self.requested_size = requested_size;
        self.geometry_size = geometry_size;
        self.texture_size = Size::default();
        self.origin_delta = Point::default();
        self.origin = None;
        self.textures.clear();
    }

    fn push(&mut self, texture: RenderTexture, location: Point<i32, Logical>) {
        let origin = self.origin.get_or_insert(location);
        origin.x = origin.x.min(location.x);
        origin.y = origin.y.min(location.y);

        // Compute the bottom-right extent as plain i32 to avoid Smithay's
        // Size::new() panic when a subsurface has a negative offset. Clamp to
        // zero: a negative extent means the subsurface is entirely off-canvas
        // and contributes nothing to the bounding box.
        let tex = texture.size();
        let extent_w = (tex.w + location.x).max(0);
        let extent_h = (tex.h + location.y).max(0);
        let max_size = Size::from((extent_w, extent_h));
        self.texture_size = self.texture_size.max(max_size);

        self.textures.push(texture);
    }

    fn reset_origin(&mut self) {
        let Some(origin) = self.origin.take().filter(|o| *o != Point::default()) else {
            return;
        };

        for texture in &mut self.textures {
            texture.offset_location(Point::default() - origin);
        }

        self.texture_size.w -= origin.x;
        self.texture_size.h -= origin.y;

        self.origin_delta += origin;
    }

    fn size(&self) -> Size<i32, Logical> {
        self.geometry_size.unwrap_or(self.texture_size)
    }
}

#[derive(Debug)]
struct PresentationCallback {
    callback: PresentationFeedbackCallback,
    surface: WlSurface,
}

impl PresentationCallback {
    fn new(surface: WlSurface, callback: PresentationFeedbackCallback) -> Self {
        Self { callback, surface }
    }
}

struct PopupConstrainer<'a>(&'a mut PositionerState);

impl PopupConstrainer<'_> {
    fn constrained_location(
        &mut self,
        bounds: Rectangle<i32, Logical>,
        popup_size: Size<i32, Logical>,
    ) -> Point<i32, Logical> {
        let mut location = self.0.get_geometry().loc;

        let rect = Rectangle::new(location, popup_size);
        if bounds.contains_rect(rect) {
            return location;
        }

        let flip_y = self
            .constraint_adjustment
            .contains(ConstraintAdjustment::FlipY);
        let flip_x = self
            .constraint_adjustment
            .contains(ConstraintAdjustment::FlipX);
        let old_anchor = self.anchor_edges;
        let old_gravity = self.gravity;

        let mut flip = |bounds_loc, bounds_size, loc, size, x_axis| {
            if loc < bounds_loc || loc + size > bounds_loc + bounds_size {
                self.anchor_edges = Self::flip_anchor(self.anchor_edges, x_axis);
                self.gravity = Self::flip_gravity(self.gravity, x_axis);
            }
        };

        let slide = |bounds_loc, bounds_size, loc, size| {
            if loc < bounds_loc && loc + size < bounds_loc + bounds_size {
                cmp::min(bounds_loc + bounds_size - size, bounds_loc)
            } else if loc > bounds_loc && loc + size > bounds_loc + bounds_size {
                cmp::max(bounds_loc + bounds_size - size, bounds_loc)
            } else {
                loc
            }
        };

        if flip_x {
            flip(bounds.loc.x, bounds.size.w, location.x, popup_size.w, true);
        }
        if flip_y {
            flip(bounds.loc.y, bounds.size.h, location.y, popup_size.h, false);
        }

        let new_location = self.get_geometry().loc;
        let new_rect = Rectangle::new(new_location, popup_size);
        if bounds.contains_rect(new_rect) {
            return new_location;
        }

        self.anchor_edges = old_anchor;
        self.gravity = old_gravity;

        if self
            .constraint_adjustment
            .contains(ConstraintAdjustment::SlideX)
        {
            location.x = slide(bounds.loc.x, bounds.size.w, location.x, popup_size.w);
        }
        if self
            .constraint_adjustment
            .contains(ConstraintAdjustment::SlideY)
        {
            location.y = slide(bounds.loc.y, bounds.size.h, location.y, popup_size.h);
        }

        location
    }

    fn flip_anchor(anchor: xdg_positioner::Anchor, x_axis: bool) -> xdg_positioner::Anchor {
        match (anchor, x_axis) {
            (xdg_positioner::Anchor::Right, true) => xdg_positioner::Anchor::Left,
            (xdg_positioner::Anchor::TopRight, true) => xdg_positioner::Anchor::TopLeft,
            (xdg_positioner::Anchor::BottomRight, true) => xdg_positioner::Anchor::BottomLeft,
            (xdg_positioner::Anchor::Left, true) => xdg_positioner::Anchor::Right,
            (xdg_positioner::Anchor::TopLeft, true) => xdg_positioner::Anchor::TopRight,
            (xdg_positioner::Anchor::BottomLeft, true) => xdg_positioner::Anchor::BottomRight,
            (xdg_positioner::Anchor::Top, false) => xdg_positioner::Anchor::Bottom,
            (xdg_positioner::Anchor::TopRight, false) => xdg_positioner::Anchor::BottomRight,
            (xdg_positioner::Anchor::TopLeft, false) => xdg_positioner::Anchor::BottomLeft,
            (xdg_positioner::Anchor::Bottom, false) => xdg_positioner::Anchor::Top,
            (xdg_positioner::Anchor::BottomRight, false) => xdg_positioner::Anchor::TopRight,
            (xdg_positioner::Anchor::BottomLeft, false) => xdg_positioner::Anchor::TopLeft,
            (anchor, _) => anchor,
        }
    }

    fn flip_gravity(gravity: Gravity, x_axis: bool) -> Gravity {
        match (gravity, x_axis) {
            (Gravity::Right, true) => Gravity::Left,
            (Gravity::TopRight, true) => Gravity::TopLeft,
            (Gravity::BottomRight, true) => Gravity::BottomLeft,
            (Gravity::Left, true) => Gravity::Right,
            (Gravity::TopLeft, true) => Gravity::TopRight,
            (Gravity::BottomLeft, true) => Gravity::BottomRight,
            (Gravity::Top, false) => Gravity::Bottom,
            (Gravity::TopRight, false) => Gravity::BottomRight,
            (Gravity::TopLeft, false) => Gravity::BottomLeft,
            (Gravity::Bottom, false) => Gravity::Top,
            (Gravity::BottomRight, false) => Gravity::TopRight,
            (Gravity::BottomLeft, false) => Gravity::TopLeft,
            (gravity, _) => gravity,
        }
    }
}

impl Deref for PopupConstrainer<'_> {
    type Target = PositionerState;

    fn deref(&self) -> &Self::Target {
        self.0
    }
}

impl DerefMut for PopupConstrainer<'_> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        self.0
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn positioner_constraints_slide() {
        let mut positioner = dummy_positioner();

        positioner.constraint_adjustment =
            ConstraintAdjustment::SlideX | ConstraintAdjustment::SlideY;
        positioner.offset = (95, 95).into();
        assert_eq!(constrain(&mut positioner), (90, 90).into());

        positioner.offset = (-5, -5).into();
        assert_eq!(constrain(&mut positioner), (0, 0).into());

        positioner.rect_size = (1000, 1000).into();
        positioner.offset = (-100, -100).into();
        assert_eq!(constrain(&mut positioner), (-100, -100).into());
    }

    fn dummy_positioner() -> PositionerState {
        PositionerState {
            rect_size: (10, 10).into(),
            anchor_rect: Rectangle::from_size((10, 10).into()),
            anchor_edges: xdg_positioner::Anchor::TopLeft,
            gravity: Gravity::BottomRight,
            constraint_adjustment: ConstraintAdjustment::None,
            ..Default::default()
        }
    }

    fn constrain(positioner: &mut PositionerState) -> Point<i32, Logical> {
        PopupConstrainer(&mut positioner.clone()).constrained_location(
            Rectangle::from_size(Size::from((100, 100))),
            positioner.rect_size,
        )
    }
}
