//! Close animation on `Windows`: when a toplevel is destroyed its last frame
//! is kept as a shrinking tombstone for a brief moment, so windows fade out of
//! their slot instead of vanishing. The live `closing_windows` list lives on
//! `Windows`; this owns the capture, the per-frame tick and the rendering.

use std::time::Duration;

use super::*;

/// Tombstone for the close animation. Captured *before* `Layouts::reap`
/// drops the window, so the texture clone survives even though the
/// originating wl_surface has been destroyed.
#[derive(Debug)]
pub(super) struct ClosingWindow {
    /// Last-rendered texture from the destroyed surface. `RenderTexture`
    /// is `Rc<Texture>` so cloning it is a refcount bump and the GL handle
    /// is kept alive for as long as this entry lives.
    texture:     crate::drawing::RenderTexture,
    /// On-screen rect (logical px) at the moment of destruction. Anchors
    /// the shrink so the animation starts from where the window was sitting.
    target_rect: Rectangle<i32, Logical>,
    start:       Instant,
    duration:    Duration,
}

/// Total duration of the close animation. Short enough that the user does
/// not perceive it as a delay between clicking close and the slot freeing
/// up, long enough that the eye registers the shrink.
const CLOSE_ANIM_MS: u64 = 160;

impl Windows {
    /// Build a [`ClosingWindow`] entry for the destroyed `surface`, returning
    /// `None` when the window has no rendered texture yet (a destroy that
    /// races the first frame, very rare in practice). Pure read — no state
    /// mutation — so the caller decides whether to push the result onto the
    /// `closing_windows` list.
    pub(super) fn snapshot_for_close(&self, surface: &ToplevelSurface) -> Option<ClosingWindow> {
        let window_rc = self.layouts.find_window(surface.surface())?;
        let w = window_rc.borrow();
        if w.minimized { return None; }
        let texture = w.thumbnail_texture()?;
        // Use output_rectangle (the compositor-tracked position/size) rather
        // than bounds() so the animation anchor is stable even when the
        // texture_cache has already been flushed by a null-buffer commit
        // (foot terminal pattern). bounds() adds an internal_offset derived
        // from texture_cache.size(), which is (0,0) after the null commit,
        // shifting the rect down by half the window height.
        let target_rect = w.allocated_rect();
        Some(ClosingWindow {
            texture,
            target_rect,
            start:    Instant::now(),
            duration: Duration::from_millis(CLOSE_ANIM_MS),
        })
    }

    /// Drop closing-window entries whose timer has expired. Called from the
    /// frame tick alongside the other animation pumps.
    pub fn tick_closing_animations(&mut self) {
        let now = Instant::now();
        let before = self.closing_windows.len();
        self.closing_windows
            .retain(|c| now.saturating_duration_since(c.start) < c.duration);
        if self.closing_windows.len() != before {
            self.dirty = true;
        }
    }

    /// Render every active closing-window tombstone into `textures`. Each
    /// entry shrinks from full size to zero on a quadratic ease-in over
    /// `CLOSE_ANIM_MS`, anchored on the centre of its original on-screen
    /// rect so the user sees the window pull inward toward the spot it
    /// used to occupy. Free function (no `self`) so the caller can hand
    /// in `&mut self.textures` and `&self.closing_windows` from inside
    /// `textures()` without a borrow conflict — Rust's split-borrow
    /// inference doesn't reach across method calls.
    pub(super) fn render_closing_windows(
        closing: &[ClosingWindow],
        scale:   f64,
        textures: &mut Vec<ForgeElement>,
    ) {
        let now = Instant::now();
        for c in closing {
            let elapsed = now.saturating_duration_since(c.start).as_secs_f32();
            let total   = c.duration.as_secs_f32().max(0.001);
            let t       = (elapsed / total).clamp(0.0, 1.0);
            // Quadratic ease-in: starts at near-full size and accelerates
            // inward. Visually feels like the window collapses *into*
            // itself instead of sliding away linearly.
            let shrink = 1.0 - (t * t);
            if shrink <= 0.001 { continue; }

            let rect_phys: smithay::utils::Rectangle<i32, smithay::utils::Physical> =
                c.target_rect.to_physical_precise_round(scale);
            let cx = rect_phys.loc.x + rect_phys.size.w / 2;
            let cy = rect_phys.loc.y + rect_phys.size.h / 2;
            let new_w = (rect_phys.size.w as f32 * shrink) as i32;
            let new_h = (rect_phys.size.h as f32 * shrink) as i32;
            if new_w <= 0 || new_h <= 0 { continue; }

            let new_loc = smithay::utils::Point::<i32, smithay::utils::Physical>::from((
                cx - new_w / 2,
                cy - new_h / 2,
            ));
            // Pin the texture's window_scale to `shrink` so smithay's
            // RescaleRenderElement does the actual pixel-side resampling;
            // pass the shrunk rect as the crop bounds so opaque-region
            // tracking stays in sync with what is actually drawn.
            let bounds = smithay::utils::Rectangle::<i32, smithay::utils::Physical>::new(
                new_loc, (new_w, new_h).into(),
            );
            ForgeElement::add_element(
                textures,
                c.texture.clone(),
                new_loc,
                bounds,
                Some(shrink as f64 * scale),
                scale,
            );
        }
    }
}
