//! One mapped window: a client surface (toplevel, popup, layer or X11-free
//! generic `S`) with its textures, transactions and popups. `render` builds
//! the element passes and thumbnails, `commit` handles buffer import and
//! surface commits, `popup` the popup tree and hit-testing, `kinds` the
//! surface-specific impls.

mod commit;
mod kinds;
mod popup;
mod render;

pub(crate) use commit::take_surface_tree_buffers;
use commit::{PresentationCallback, TextureCache, WindowTransaction};

use std::cell::RefCell;
use std::mem;
use std::sync::Mutex;
use std::time::Instant;

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::allocator::Fourcc;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::ImportAll;
use smithay::reexports::wayland_protocols::wp::presentation_time::server as _presentation_time;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::Resource;
use smithay::utils::{Buffer as BufferCoord, 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, 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};


/// How a toplevel's decorations are chosen. `Auto` defers to the `csd_apps`
/// app-id allowlist; the explicit variants record an xdg-decoration request
/// from the client and take precedence over the allowlist (which is only a
/// fallback for clients that never bind the protocol).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecorationPolicy {
    Auto,
    ClientSide,
    ServerSide,
}

#[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>>,

    /// Work area at the time of [`Self::enter_fullscreen`]; consumed on
    /// un-fullscreen to detect whether the area moved while fullscreen.
    pre_fullscreen_avail: 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,

    /// Decoration mode for this window. `ClientSide` (whether requested via
    /// zxdg_toplevel_decoration_v1 or matched by the `csd_apps` allowlist)
    /// skips forge's SSD so the client's own title bar is the only one
    /// visible; an explicit `ServerSide` request keeps the SSD even when the
    /// app-id matches the allowlist.
    pub decoration: DecorationPolicy,

    pub(crate) ssd: Option<Box<crate::ssd::SsdDecoration>>,
    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>,
    /// App-switcher thumbnail snapshot: the live window content blitted once
    /// into a plain offscreen texture. The carousel composites this instead of
    /// the live client buffer, because scaling a dmabuf-backed (external) GLES
    /// texture in the panel pipeline renders black on some GPUs (Librem 5 /
    /// etnaviv) — the snapshot is an ordinary 2D texture that scales fine.
    /// Invalidated (set to `None`) whenever the window commits new content.
    thumbnail_snapshot: 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,

    /// True while a floating window's frame tracks the client's committed
    /// `xdg_surface.set_window_geometry`. Stays set for the window's life so a
    /// client that ignores our configures (suggested fallback size, or a stale
    /// per-app rect re-applied on restore) and renders smaller never keeps an
    /// oversized SSD bar. Cleared when the user takes manual control via an
    /// interactive resize.
    pub auto_sizing: bool,

    /// The one-shot 1024x720 fallback configure has been sent. After it, the
    /// frame follows whatever geometry the client commits, so a client that
    /// insists on a small size keeps a matching frame.
    fallback_sent: bool,

    /// xdg-session-management binding: session id + toplevel name the
    /// window's WM state is recorded under, and any state waiting to be
    /// applied on the first commit.
    pub session: Option<crate::protocols::session_management::WindowSession>,

    /// Saved geometry whose screen was not connected when it was applied;
    /// re-applied when that screen appears, forgotten on a manual move.
    pub deferred_geo: Option<crate::geometry_store::SavedGeo>,
}


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,
            thumbnail_snapshot: 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,
            pre_fullscreen_avail: None,
            minimized: false,
            decoration: DecorationPolicy::Auto,
            foreign_handle: None,
            needs_geometry_restore: false,
            auto_sizing: false,
            fallback_sent: false,
            session: None,
            deferred_geo: None,
            ssd: None,
        };

        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 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 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.auto_sizing = false;
        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;
    }

    /// Reconcile this window's membership of one screen's wl_output —
    /// enter when overlapping it, leave otherwise. Unlike [`Self::enter`] /
    /// [`Self::leave`] this does not touch the `visible` flag; smithay
    /// deduplicates the protocol events, so redundant calls are free.
    pub fn sync_output_overlap(&self, output: &Output, inside: bool) {
        if inside {
            self.with_surfaces(|s, _| output.enter(s));
        } else {
            self.with_surfaces(|s, _| output.leave(s));
        }
    }

    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,
        );
    }


    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()
    }
}
