//! Window scene composition: the primary and per-extra-output texture passes,
//! frame-callback dispatch and presentation feedback. Reads window/layer state
//! on `Windows`.

use smithay::backend::drm::DrmEventMetadata;
use smithay::backend::renderer::element::{Element, RenderElementStates};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Rectangle};
use smithay::wayland::compositor::{self, TraversalAction};

use crate::drawing::{ForgeElement, Graphics};
use crate::windows::cursor::push_cursor_surface;

use super::*;

impl Windows {
    #[cfg_attr(feature = "profiling", profiling::function)]
    pub fn textures(
        &mut self,
        renderer: &mut GlesRenderer,
        graphics: &mut Graphics,
        cursor: Option<crate::cursor::CursorRender>,
        ctx: &mut crate::render_context::RenderContext,
    ) -> &[ForgeElement] {
        let text_renderer = &mut *ctx.text;
        let mpris = &mut *ctx.mpris;
        let polkit = &mut *ctx.polkit;
        let osd = &mut *ctx.osd;
        let topbar = &mut *ctx.topbar;
        let sys_state = &*ctx.sys;
        let app_switcher = &mut *ctx.switcher;
        let screenshot = &mut *ctx.screenshot;

        self.dirty = false;

        let scale = self.output.scale();
        self.textures.clear();
        graphics.tick_cursor_theme();

        // The pointer belongs to exactly one screen's scene; while it sits on
        // an extend head the primary must not also paint it.
        let cursor = cursor.filter(|c| {
            self.extra_outputs.is_empty() || self.output.logical_rect().to_f64().contains(c.position)
        });

        // textures[0] is the front-most layer, so push the cursor before
        // every other overlay to keep it painted on top.
        if let Some(cursor) = cursor {
            // forge owns the resize edges of its own decorations, so a resize
            // shape there overrides whatever the client requested. The
            // screenshot overlay owns the pointer outright, so its cursor
            // wins, and a top/overlay layer surface covering the point
            // (calendar, crustace panels) stacks above the resize band —
            // matching the input paths' layer exemption.
            let override_icon = match self.resize_cursor_edge(cursor.position) {
                Some(edge) if !screenshot.is_active()
                    && !self.foreground_layer_at(cursor.position) =>
                {
                    Some(crate::drawing::resize_icon(edge))
                }
                _ => None,
            };
            let raw: Point<i32, Physical> = cursor.position.to_physical(scale).to_i32_round();
            if push_cursor_element(
                &mut self.textures, renderer, graphics, &self.canvas,
                raw, &cursor.image, override_icon, scale,
            ) {
                self.dirty = true;
            }
        }

        // Region-screenshot overlay — modal capture chrome above all UI, kept
        // below the cursor so the drag origin stays visible.
        screenshot.render(&mut self.textures, &self.canvas, renderer, self.output.logical_rect().loc);

        match &self.view {
            View::Workspace => {
                for layer in self.layers.foreground() {
                    layer.textures(&mut self.textures, scale, None, None);
                }
                let bar_w = self.canvas.size().w;
                topbar.render(&mut self.textures, &self.canvas, renderer, text_renderer, sys_state, bar_w);
            }
            _ => {}
        }

        if app_switcher.is_active() {
            self.snapshot_switcher_thumbnails(renderer);
            let entries = self.app_switcher_entries();
            app_switcher.refresh_entries(entries);
        } else {
            app_switcher.prewarm(renderer, &self.canvas);
        }
        app_switcher.tick_theme();
        app_switcher.tick_carousel();
        app_switcher.render(&mut self.textures, &self.canvas, renderer, text_renderer);
        if app_switcher.animating() {
            self.dirty = true;
        }

        // Polkit authentication dialog — modal, on top of everything
        polkit.textures(&mut self.textures, &self.canvas, renderer, text_renderer);

        // Portal consent dialog — modal, on top of everything
        self.confirm.textures(&mut self.textures, &self.canvas, renderer, text_renderer);

        // MPRIS media controls (normal mode, not lockscreen)
        mpris.textures(&mut self.textures, &self.canvas, renderer, text_renderer, false);

        // OSD volume/brightness overlay — on top of everything else, on the
        // screen holding the pointer.
        let pointer_on_primary = self.extra_outputs.is_empty()
            || self
                .pointer_pos
                .map(|p| self.output.logical_rect().to_f64().contains(p))
                .unwrap_or(true);
        if pointer_on_primary {
            osd.textures(&mut self.textures, &self.canvas, renderer, text_renderer);
        }

        // Snap-preview overlay — translucent accent rectangle over the
        // zone the in-flight modifier-click drag would commit to on
        // release. Drawn above everything else so the user sees the
        // target even when dragging across foreground layer surfaces
        // (e.g. crustace overlays). No-op when no snap-enabled drag is
        // in flight or the cursor is not near a snap edge.
        if let Some((zone, avail)) = self.active_snap() {
            if avail.overlaps(self.output.logical_rect()) {
                let target = zone.rect(avail);
                let phys = target.to_physical_precise_round(scale);
                ForgeElement::add_element(
                    &mut self.textures,
                    graphics.snap_preview.clone(),
                    phys.loc, phys, None, scale,
                );
            }
        }

        // Active-corner feedback: an accent glow centred on the vertex, growing
        // with the push.
        if let Some((corner, local)) = self.corner_feedback_rect(self.output.logical_rect()) {
            let phys = local.to_physical_precise_round(scale);
            let glow_scale = local.size.w as f64 / Graphics::CORNER_GLOW_TEX as f64;
            ForgeElement::add_element(
                &mut self.textures,
                graphics.corner_glow(renderer, corner),
                phys.loc, phys, glow_scale, scale,
            );
        }

        if self.canvas.is_mobile() && !matches!(self.view, View::Fullscreen(_) | View::Lock(_)) {
            let gesture_handle = graphics.gesture_handle(renderer, &self.canvas);

            let mut bounds = gesture_handle.geometry(scale.into());
            let output_height = self.canvas.physical_size().h;
            let handle_location = (0, output_height - bounds.size.h);
            bounds.loc = handle_location.into();

            ForgeElement::add_element(
                &mut self.textures,
                gesture_handle,
                handle_location,
                bounds,
                None,
                scale,
            );
        }

        let needs_wallpaper = matches!(self.view, View::Workspace);

        match &mut self.view {
            View::Workspace => {
                if self.canvas.is_mobile() {
                    self.layouts.textures_and_ssd(
                        &mut self.textures, &self.canvas, renderer, text_renderer,
                        &self.csd_apps,
                        self.pointer_pos,
                    );
                    self.x11_windows.paint_all(
                        &mut self.textures, &self.canvas, renderer, text_renderer,
                        self.pointer_pos,
                    );
                } else {
                    use std::collections::HashSet;
                    let order = self.toplevel_z_order.clone();
                    let mut painted: HashSet<u32> = HashSet::new();
                    use smithay::reexports::wayland_server::Resource as _;
                    for wl in order.iter().rev() {
                        if self.layouts.paint_one(
                            wl, &mut self.textures, &self.canvas, renderer, text_renderer,
                            &self.csd_apps, self.pointer_pos,
                        ) { painted.insert(wl.id().protocol_id()); continue; }
                        if self.x11_windows.paint_one(
                            wl, &mut self.textures, &self.canvas, renderer, text_renderer,
                            self.pointer_pos,
                        ) { painted.insert(wl.id().protocol_id()); }
                    }
                    self.layouts.paint_remaining(
                        &painted, &mut self.textures, &self.canvas, renderer, text_renderer,
                        &self.csd_apps, self.pointer_pos,
                    );
                    self.x11_windows.paint_remaining(
                        &painted, &mut self.textures, &self.canvas, renderer, text_renderer,
                        self.pointer_pos,
                    );
                }

                // Closing-window tombstones render after live windows so
                // their shrink animation paints on top of whatever just
                // appeared underneath, but below the foreground layer-shell
                // strip so a closing app does not bleed through the topbar.
                Self::render_closing_windows(&self.closing_windows, scale, &mut self.textures);

                for layer in self.layers.background() {
                    layer.textures(&mut self.textures, scale, None, None);
                }
            },
            View::Fullscreen(window) => {
                for layer in self.layers.overlay() {
                    layer.textures(&mut self.textures, scale, None, None);
                }

                window.borrow().textures(&mut self.textures, scale, None, None);
            },
            View::Lock(window_opt) => {
                // On-screen keyboard above the lock. A session-lock compositor
                // otherwise presents only the lock surface, so squeekboard's
                // layer surface (namespace "osk", on top/overlay) would never
                // show while locked. Composited first so it sits above the lock
                // surface; everything else on the foreground layers stays
                // hidden, as the lock spec requires.
                for osk in self.layers.foreground().filter(|w| w.app_id.as_deref() == Some("osk")) {
                    osk.textures(&mut self.textures, scale, None, None);
                }
                // Lock-surface texture next; the solid fallback follows so it
                // sits *behind* the surface. Without the fallback the previous
                // frame would stay on screen until the lock client commits its
                // first buffer, and would also stay on screen forever if the
                // client died — the ext-session-lock-v1 spec mandates that the
                // session stays visually locked in both cases.
                if let Some(window) = window_opt {
                    window.textures(&mut self.textures, scale, None, None);
                }
                let size_log = self.canvas.size();
                let logical: Rectangle<i32, Logical> =
                    Rectangle::new(Point::from((0, 0)), size_log);
                let phys = logical.to_physical_precise_round(scale);
                ForgeElement::add_element(
                    &mut self.textures,
                    graphics.lock_fallback.clone(),
                    phys.loc, phys, None, scale,
                );
            },
        }

        // Wallpaper — rendered behind everything as the bottom-most layer
        if needs_wallpaper {
            self.ensure_wallpaper(renderer);
            if let Some(wp) = &self.wallpaper {
                let size = self.canvas.size();
                let wp_rect: Rectangle<i32, Logical> =
                    Rectangle::new(Point::from((0, 0)), size);
                let physical_wp = wp_rect.to_physical_precise_round(scale);
                ForgeElement::add_element(
                    &mut self.textures,
                    wp.clone(),
                    physical_wp.loc,
                    physical_wp,
                    None,
                    scale,
                );
            }
        }

        self.textures.as_slice()
    }

    /// Slim per-screen scene for an extend head: the desktop windows whose
    /// global rect overlaps the screen, translated into its local space and
    /// rasterised at its scale, plus the cursor while the pointer is on it
    /// and the wallpaper. Chrome (topbar, layers, OSD, switcher) stays on
    /// the primary. While locked the head shows only the lock fallback.
    pub fn textures_for_extra_output(
        &mut self,
        renderer: &mut GlesRenderer,
        graphics: &mut Graphics,
        name: &str,
        cursor: Option<crate::cursor::CursorRender>,
        ctx: &mut crate::render_context::RenderContext,
    ) -> Vec<ForgeElement> {
        let text_renderer = &mut *ctx.text;
        let osd = &mut *ctx.osd;
        let app_switcher = &mut *ctx.switcher;
        let screenshot = &mut *ctx.screenshot;

        let Some(head) = self
            .extra_outputs
            .iter()
            .find(|o| o.smithay_output().name() == name)
        else {
            return Vec::new();
        };
        let head_rect = head.logical_rect();
        let canvas = *head.canvas();
        let scale = canvas.scale();
        let mut textures = Vec::new();

        let locked = matches!(self.view, View::Lock(_));

        // Cursor first so it stays painted on top; these heads have no
        // hardware cursor plane yet — while locked too, or the pointer
        // vanishes on this screen. Same resize-edge override as the
        // primary scene: forge owns the grab bands of its decorations, so
        // the resize shape must win there over the client's cursor. No
        // override while locked: there are no window edges to grab.
        if let Some(cursor) = cursor.filter(|c| head_rect.to_f64().contains(c.position)) {
            let override_icon = match self.resize_cursor_edge(cursor.position) {
                Some(edge) if !locked && !self.foreground_layer_at(cursor.position) => {
                    Some(crate::drawing::resize_icon(edge))
                }
                _ => None,
            };
            let local = cursor.position - head_rect.loc.to_f64();
            let raw: Point<i32, Physical> = local.to_physical(scale).to_i32_round();
            if push_cursor_element(
                &mut textures, renderer, graphics, &canvas,
                raw, &cursor.image, override_icon, scale,
            ) {
                self.dirty = true;
            }
        }

        // Region-screenshot overlay: this screen's local view of the global
        // selection, below the cursor like on the primary.
        if !locked {
            screenshot.render(&mut textures, &canvas, renderer, head_rect.loc);
        }

        if locked {
            // This screen's own lock surface next, the solid fallback
            // behind it — same order and reasoning as the primary: the
            // session must stay visually locked when the client lags or
            // dies.
            if let Some((_, window)) = self.extra_locks.iter().find(|(n, _)| n == name) {
                window.textures(&mut textures, scale, None, None);
            }
            let phys = Rectangle::from_size(canvas.physical_resolution());
            ForgeElement::add_element(
                &mut textures,
                graphics.lock_fallback.clone(),
                phys.loc, phys, None, scale,
            );
            return textures;
        }

        // Snap-preview overlay for a drag currently targeting this screen.
        if let Some((zone, avail)) = self.active_snap() {
            if avail.overlaps(head_rect) {
                let target = zone.rect(avail);
                let local = Rectangle::new(target.loc - head_rect.loc, target.size);
                let phys = local.to_physical_precise_round(scale);
                ForgeElement::add_element(
                    &mut textures,
                    graphics.snap_preview.clone(),
                    phys.loc, phys, None, scale,
                );
            }
        }

        if let Some((corner, local)) = self.corner_feedback_rect(head_rect) {
            let phys = local.to_physical_precise_round(scale);
            let glow_scale = local.size.w as f64 / Graphics::CORNER_GLOW_TEX as f64;
            ForgeElement::add_element(
                &mut textures,
                graphics.corner_glow(renderer, corner),
                phys.loc, phys, glow_scale, scale,
            );
        }

        // OSD volume/brightness overlay follows the pointer's screen.
        let pointer_here = self
            .pointer_pos
            .map(|p| head_rect.to_f64().contains(p))
            .unwrap_or(false);
        if pointer_here {
            osd.textures(&mut textures, &canvas, renderer, text_renderer);
        }

        // The app switcher shows on every screen, each with its own windows
        // (Task View style), riding the frame clock the primary advanced.
        // Internally a no-op while the panel is not up, and it keeps
        // painting through the fade-out exactly like the primary's.
        app_switcher.render_extra(
            &mut textures, &canvas, renderer, text_renderer, name, head_rect.loc,
        );

        // Desktop windows overlapping this screen, topmost first — Wayland
        // toplevels and Xwayland ones in one stacking walk.
        let order = self.toplevel_z_order.clone();
        for wl in order.iter().rev() {
            if self.layouts.paint_one_translated(
                wl, &mut textures, &canvas, head_rect, renderer, text_renderer,
                &self.csd_apps, self.pointer_pos,
            ) {
                continue;
            }
            self.x11_windows.paint_one_translated(
                wl, &mut textures, &canvas, Some(head_rect), renderer, text_renderer,
                self.pointer_pos,
            );
        }

        // The visible desktop background: the shell's own background layer
        // bound to this output when it created one (drawn in this head's
        // local frame at its own scale), else the primary's background
        // cover-scaled so both desktops share one wallpaper — kept as the
        // fallback for shells that do not cover extra outputs. The baked
        // wallpaper file below remains the last resort for shell-less
        // sessions.
        let mut shell_bg = false;
        for layer in self.layers.background_on(name) {
            layer.textures(&mut textures, scale, None, None);
            shell_bg = true;
        }
        let primary_log = self.output.logical_rect().size;
        if !shell_bg && primary_log.w > 0 && primary_log.h > 0 {
            // Rescale the layer's content around the origin to cover this
            // screen, and crop against the full head — the default bounds
            // would crop at the layer's own (primary-sized) rect.
            let cover = (head_rect.size.w as f64 / primary_log.w as f64)
                .max(head_rect.size.h as f64 / primary_log.h as f64);
            let head_phys = Rectangle::from_size(canvas.physical_size());
            for layer in self.layers.background() {
                layer.textures_with_bounds(
                    &mut textures,
                    scale,
                    cover,
                    Point::default(),
                    head_phys,
                    None,
                );
                shell_bg = true;
            }
        }
        if shell_bg {
            return textures;
        }

        // Wallpaper baked to this screen's own physical size and scale,
        // cached per connector — the primary's raster has another size and
        // would come out cropped or stretched.
        let phys_size = canvas.physical_size();
        let stale = match self.extra_wallpapers.get(name) {
            Some((_, size, s)) => *size != phys_size || (*s - scale).abs() >= 0.01,
            None => true,
        };
        if stale {
            let baked = Self::bake_wallpaper(renderer, scale, phys_size);
            self.extra_wallpapers
                .insert(name.to_string(), (baked, phys_size, scale));
        }
        if let Some((Some(wp), _, _)) = self.extra_wallpapers.get(name) {
            let phys = Rectangle::from_size(phys_size);
            ForgeElement::add_element(&mut textures, wp.clone(), phys.loc, phys, None, scale);
        }

        textures
    }

    pub fn request_frames(&mut self, switcher_active: bool) {
        let runtime = self.runtime();
        self.x11_windows.request_frames(runtime);

        // An animated client cursor advances on its own frame callbacks; without
        // these it would sit on its first frame forever.
        if let Some(surface) = &self.cursor_surface {
            compositor::with_surface_tree_downward(
                surface,
                (),
                |_, _, _| TraversalAction::DoChildren(()),
                |_, states, _| {
                    let mut attrs = states.cached_state.get::<compositor::SurfaceAttributes>();
                    for callback in attrs.current().frame_callbacks.drain(..) {
                        callback.done(runtime);
                    }
                },
                |_, _, _| true,
            );
        }

        match &self.view {
            View::Fullscreen(window) => {
                for overlay in self.layers.overlay() {
                    overlay.request_frame(runtime);
                }
                window.borrow().request_frame(runtime);
            },
            View::Workspace => {
                self.layers.request_frames(runtime);
                if self.canvas.is_mobile() && !switcher_active {
                    self.layouts.with_visible(|w| w.request_frame(runtime));
                } else {
                    // Frame callbacks must go to every window, not just the
                    // active one, in two cases:
                    // - Desktop stacks every toplevel in its own Layout and
                    //   renders all simultaneously; a non-"active" window must
                    //   still get callbacks or it can't commit (typed text
                    //   never appears until the active set is nudged).
                    // - The app switcher is up: each off-centre carousel
                    //   thumbnail must keep committing so background video
                    //   keeps playing in its tile, not just the centred one.
                    for window in self.layouts.windows() {
                        window.request_frame(runtime);
                    }
                }
            },
            View::Lock(window_opt) => {
                if let Some(window) = window_opt {
                    window.request_frame(runtime);
                }
                for (_, window) in &self.extra_locks {
                    window.request_frame(runtime);
                }
                // Keep squeekboard drawing while locked (see the lock render arm).
                for osk in self.layers.foreground().filter(|w| w.app_id.as_deref() == Some("osk")) {
                    osk.request_frame(runtime);
                }
            },
        }
    }

    pub fn mark_presented(
        &mut self,
        states: &RenderElementStates,
        metadata: &Option<DrmEventMetadata>,
    ) {
        for mut window in self.layouts.windows_mut() {
            window.mark_presented(states, metadata, &self.output, &self.start_time);
        }

        for layer in self.layers.iter_mut() {
            layer.mark_presented(states, metadata, &self.output, &self.start_time);
        }
    }
}

/// Composite `cursor`'s image into `textures` at `raw` (output-local physical
/// coordinates), honouring `override_icon` for forge-owned grab bands. Shared
/// by the primary scene and the extend-head generator — only the override
/// rules and the coordinate origin differ per caller. Returns whether the
/// cursor theme is animating, so the caller keeps the scene dirty.
fn push_cursor_element(
    textures: &mut Vec<ForgeElement>,
    renderer: &mut GlesRenderer,
    graphics: &mut Graphics,
    canvas: &Canvas,
    raw: Point<i32, Physical>,
    image: &crate::cursor::CursorImage,
    override_icon: Option<cursor_icon::CursorIcon>,
    scale: f64,
) -> bool {
    match (override_icon, image) {
        (None, crate::cursor::CursorImage::Surface { surface, hotspot }) => {
            let origin = raw - hotspot.to_physical_precise_round(scale);
            push_cursor_surface(textures, surface, origin, scale);
            false
        }
        (over, image) => {
            let icon = over.unwrap_or(match image {
                crate::cursor::CursorImage::Named(icon) => *icon,
                _ => cursor_icon::CursorIcon::Default,
            });
            let Some((texture, hotspot)) = graphics.cursor_for(renderer, canvas, icon) else {
                return false;
            };
            let pos = raw - hotspot;
            let mut bounds = texture.geometry(scale.into());
            bounds.loc = pos;
            ForgeElement::add_cursor(textures, texture, pos, bounds, None, scale);
            graphics.cursor_animating()
        }
    }
}

/// Radius, in logical pixels, of the fully pushed corner glow.
const CORNER_GLOW_RADIUS: f64 = 160.0;

impl Windows {
    /// Corner being pushed on `head` and the square (in `head`'s local frame)
    /// its glow quadrant covers; `None` when no push is under way there.
    fn corner_feedback_rect(
        &self,
        head: Rectangle<i32, Logical>,
    ) -> Option<(crate::corner_action::Corner, Rectangle<i32, Logical>)> {
        use crate::corner_action::Corner;
        let (corner, progress) = self.corner_feedback?;
        let pointer = self.pointer_pos?;
        if !head.to_f64().contains(pointer) {
            return None;
        }
        let side = ((CORNER_GLOW_RADIUS * progress).round() as i32).max(1);
        let (w, h) = (head.size.w, head.size.h);
        let loc = match corner {
            Corner::TopLeft => (0, 0),
            Corner::TopRight => (w - side, 0),
            Corner::BottomLeft => (0, h - side),
            Corner::BottomRight => (w - side, h - side),
        };
        Some((corner, Rectangle::new(loc.into(), (side, side).into())))
    }
}
