//! Buffer import, surface commits, scale handling and the per-window
//! transaction plumbing.

use super::*;

impl<S: Surface + 'static> Window<S> {
    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;
        }

        // New content arrived: drop the stale app-switcher snapshot so it is
        // re-captured. A continuously committing window (e.g. video playback)
        // therefore keeps the carousel thumbnail live, one blit per frame.
        self.thumbnail_snapshot = None;

        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(super) 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,
        avail: Size<i32, Logical>,
        window_scales: &[(AppIdMatcher, WindowScale)],
        surface: &WlSurface,
    ) {
        self.acked_size = self.surface.acked_size();
        self.ignore_transactions_locked = false;

        // A floating window's frame tracks the client's committed
        // `set_window_geometry` — not `acked_size`, which reports back the size
        // *we* suggested even when the client acks it but renders smaller. We
        // keep tracking for the window's life (until the user takes manual
        // control via an interactive resize) so the frame and SSD bar always
        // match the real window, even when the client ignores a configure: our
        // own fallback, or a stale per-app saved rect re-applied on restore.
        // Skipped while maximized / fullscreen (the frame legitimately exceeds
        // the content) and while a resize transaction is in flight. A geometry
        // below the placeholder floor (Chromium's ~64x64, Firefox ESR's 450x200
        // first-run fallback) gets a single 1024x720 suggestion, clamped to the
        // output's available area so it cannot exceed a small screen; from then
        // on the frame follows whatever the client commits, so a client that
        // ignores the suggestion keeps a matching frame. Child dialogs are
        // never second-guessed — their negotiated size stands.
        if self.floating
            && self.auto_sizing
            && self.pre_max_rect.is_none()
            && self.pre_fullscreen_rect.is_none()
            && self.transaction.is_none()
        {
            const MIN_REAL_W: i32 = 300;
            const MIN_REAL_H: i32 = 250;
            const FALLBACK_W: i32 = 1024;
            const FALLBACK_H: i32 = 720;
            let client = self.surface.geometry().map_or_else(Size::default, |g| g.size);
            let placeholder = (client.w < MIN_REAL_W || client.h < MIN_REAL_H)
                && !self.fallback_sent
                && self.surface.parent().is_none();
            if placeholder {
                if client.w > 0 || client.h > 0 {
                    let fallback = Size::<i32, Logical>::from((
                        FALLBACK_W.min(avail.w.max(MIN_REAL_W)),
                        FALLBACK_H.min(avail.h.max(MIN_REAL_H)),
                    ));
                    self.output_rectangle.size = fallback;
                    self.size = fallback;
                    self.surface.resize(fallback);
                    self.fallback_sent = true;
                }
            } else if client.w > 0 && client.h > 0 {
                self.output_rectangle.size = client;
                self.size = client;
            }
        }

        self.dirty = true;
        take_surface_tree_buffers(surface);

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

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

}

#[derive(Debug)]
pub(super) struct WindowTransaction {
    pub(super) output_rectangle: Rectangle<i32, Logical>,
    pub(super) 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 {
    pub(super) geometry_size: Option<Size<i32, Logical>>,
    texture_size: Size<i32, Logical>,
    requested_size: Size<i32, Logical>,
    origin: Option<Point<i32, Logical>>,
    pub(super) origin_delta: Point<i32, Logical>,
    pub(super) 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;
    }

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


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

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


/// Take the pending buffer of every surface in `surface`'s tree into its
/// [`ForgeSurfaceData`]. Shared by windows and the client cursor surface,
/// which has no layout of its own but needs the same buffer bookkeeping.
pub(crate) fn take_surface_tree_buffers(surface: &WlSurface) {
    compositor::with_surface_tree_upward(
        surface,
        (),
        |_, data, _| {
            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,
    );
}

