//! The compose/flip half of the `Presenter`: scene render onto the swapchain,
//! the mirror blit path, and the swapchain/damage bookkeeping.

use super::*;

impl Presenter {
    /// Compose `elements` onto a free swapchain buffer. Returns `None` when no
    /// buffer is free (a frame is already in flight) or nothing was damaged.
    pub fn render(
        &mut self,
        renderer: &mut GlesRenderer,
        elements: &[ForgeElement],
    ) -> Option<Rendered> {
        // `Windows::textures` pushes the cursor first, so it is `elements[0]`
        // when there is one. Taking it out of the scene is what makes a pure
        // pointer move produce no damage at all — but only when the plane can
        // actually show it, so the decision and the split have to agree.
        let candidate = elements
            .first()
            .filter(|e| e.kind() == Kind::Cursor && self.cursor_fits(e));
        let (scene, cursor) = match candidate {
            Some(el) => match self.plan_cursor(renderer, el) {
                Some(update) => (&elements[1..], update),
                None => (elements, self.hide_cursor_plane()),
            },
            None => (elements, self.hide_cursor_plane()),
        };

        let slot = match self.swapchain.acquire() {
            Ok(Some(slot)) => {
                self.dry_streak = 0;
                slot
            }
            Ok(None) => {
                self.note_dry();
                return None;
            }
            Err(e) => {
                tracing::warn!("swapchain acquire failed: {e}");
                return None;
            }
        };

        let (fb, mut dmabuf) = self.slot_export(&slot)?;

        // Buffer age keys the damage tracker's history so only changed regions
        // are repainted. Sound only because render and commit run in lockstep:
        // every successful render is committed (submitting the slot), and any
        // dropped frame resets buffer ages, so this slot still holds the scene
        // from `age` frames ago.
        let age = if self.force_full_repaint { self.force_full_repaint = false; 0 } else { slot.age() as usize };
        let mut framebuffer = match renderer.bind(&mut dmabuf) {
            Ok(fb) => fb,
            Err(e) => {
                tracing::warn!("renderer bind failed: {e}");
                return None;
            }
        };

        let res = self.damage.render_output(
            renderer,
            &mut framebuffer,
            age,
            scene,
            Color32F::from([0.0, 0.0, 0.0, 1.0]),
        );
        drop(framebuffer);

        match res {
            Ok(result) => {
                let damaged = result.damage.is_some();
                // On a skip nothing was composed: keep last_dmabuf and the fence
                // pointing at the previous real frame, which is still on screen.
                let gpu_fence = if damaged { result.sync.export() } else { None };
                if damaged {
                    self.last_dmabuf = Some(dmabuf);
                } else {
                    self.force_full_repaint = true;
                }
                Some(Rendered { slot, fb, damaged, gpu_fence, states: result.states, cursor })
            }
            Err(e) => {
                tracing::warn!("render_output failed: {e}");
                self.swapchain.reset_buffers();
                None
            }
        }
    }
    /// Mirror path: copy an already-composed source buffer (the primary's last
    /// frame) into a free swapchain slot and return `(fb, slot)` ready to flip,
    /// doing the swapchain `submitted` bookkeeping. No scene recomposition and no
    /// damage tracker — a mirror just mirrors, so it can run on its own vblank
    /// without waiting for the primary's `create_frame`. Returns `None` if no
    /// buffer is free or the blit fails.
    pub fn blit_flip(
        &mut self,
        renderer: &mut GlesRenderer,
        src: &Dmabuf,
    ) -> Option<(framebuffer::Handle, Slot<GbmBuffer>)> {
        use smithay::backend::renderer::{Blit, TextureFilter};

        let slot = match self.swapchain.acquire() {
            Ok(Some(slot)) => {
                self.dry_streak = 0;
                slot
            }
            Ok(None) => {
                self.note_dry();
                return None;
            }
            Err(e) => {
                tracing::warn!("mirror swapchain acquire failed: {e}");
                return None;
            }
        };
        let (fb, mut dst) = self.slot_export(&slot)?;

        let mut src = src.clone();
        let src_fb = match renderer.bind(&mut src) {
            Ok(fb) => fb,
            Err(e) => {
                tracing::warn!("mirror src bind failed: {e}");
                return None;
            }
        };
        let mut dst_fb = match renderer.bind(&mut dst) {
            Ok(fb) => fb,
            Err(e) => {
                tracing::warn!("mirror dst bind failed: {e}");
                return None;
            }
        };
        let rect = Rectangle::from_size(self.mode_size);
        if let Err(e) = renderer.blit(&src_fb, &mut dst_fb, rect, rect, TextureFilter::Linear) {
            tracing::warn!("mirror blit failed: {e}");
            return None;
        }
        drop(src_fb);
        drop(dst_fb);

        self.swapchain.submitted(&slot);
        Some((fb, slot))
    }
    /// Decide whether this rendered frame must be flipped and, if so, do the
    /// swapchain bookkeeping and return the buffer to hand to the `Committer`.
    /// `None` means skip (nothing changed, already scanning out) — the caller
    /// must reschedule by timer since no vblank will arrive. This runs on the
    /// main thread, before the capture paths, so the swapchain stays in lockstep
    /// with the damage tracker no matter what the captures do.
    pub fn prepare_flip(&mut self, rendered: Rendered) -> Option<FlipPlan> {
        let Rendered { slot, fb, damaged, gpu_fence: _, states: _, cursor } = rendered;
        let primary = if damaged || self.needs_modeset {
            self.swapchain.submitted(&slot);
            self.needs_modeset = false;
            Some((fb, slot))
        } else {
            None
        };
        if primary.is_none() && cursor.is_none() {
            return None;
        }
        Some(FlipPlan { primary, cursor })
    }
    /// A dispatched flip failed on the KMS thread. The damage tracker already
    /// advanced its history and the swapchain slot was submitted, but the buffer
    /// never reached scan-out. Reset the buffer ages so the next render
    /// full-repaints and the history resyncs with the slots.
    pub fn flip_failed(&mut self) {
        self.swapchain.reset_buffers();
        self.force_full_repaint = true;
        self.needs_modeset = true;
    }

    pub fn reset_buffers(&mut self) {
        self.swapchain.reset_buffers();
        self.force_full_repaint = true;
        self.needs_modeset = true;
    }

    fn note_dry(&mut self) {
        self.dry_streak += 1;
        if self.dry_streak == 10 {
            tracing::warn!(
                "no free swapchain slot on {:?} for 10 consecutive renders — in-flight frames are not completing",
                self.surface.crtc()
            );
        }
    }

    pub fn reset_buffer_ages(&mut self) {
        self.force_full_repaint = true;
    }

    fn slot_export(&self, slot: &Slot<GbmBuffer>) -> Option<(framebuffer::Handle, Dmabuf)> {
        if let Some(cached) = slot.userdata().get::<SlotExport>() {
            return Some((cached.fb, cached.dmabuf.clone()));
        }

        let dmabuf = match slot.export() {
            Ok(d) => d,
            Err(e) => {
                tracing::warn!("buffer export failed: {e}");
                return None;
            }
        };

        match self.exporter.add_framebuffer(
            self.surface.device_fd(),
            ExportBuffer::Allocator(slot),
            self.use_opaque,
        ) {
            Ok(Some(gbm_fb)) => {
                let fb = *gbm_fb.as_ref();
                slot.userdata().insert_if_missing(|| SlotExport {
                    fb,
                    _gbm_fb: gbm_fb,
                    dmabuf: dmabuf.clone(),
                });
                Some((fb, dmabuf))
            }
            Ok(None) => {
                tracing::warn!("no framebuffer for swapchain buffer");
                None
            }
            Err(e) => {
                tracing::warn!("add_framebuffer failed: {e}");
                None
            }
        }
    }
}
