//! The KMS-thread half of a presenter: page-flips and the in-flight buffers.

use super::*;

/// The KMS-commit half of a presenter: page-flips a rendered buffer on its
/// `DrmSurface` and holds the in-flight buffers across their vblanks. Every
/// field is `Send`, so this struct lives on the dedicated KMS thread while the
/// swapchain and renderer stay on the main thread.
pub struct Committer {
    surface: Arc<DrmSurface>,
    /// Buffer waiting on its flip's vblank.
    pending: Option<Slot<GbmBuffer>>,
    /// Buffer currently being scanned out. Held so the swapchain never hands it
    /// back for rendering while the CRTC is still displaying it — reusing it
    /// mid-scanout is what tears (coloured lines on movement).
    current: Option<Slot<GbmBuffer>>,
    /// True until the first successful modeset commit.
    needs_modeset: bool,
    /// Set while the libseat session is paused (VT switch / suspend). The main
    /// thread has dropped DRM master, so any `page_flip`/`commit` here would hit
    /// EACCES and could corrupt KMS state; `flip` becomes a no-op until `resume`.
    paused: bool,
    /// CRTC mode size in real pixels — the plane src/dst. Distinct from the
    /// logical `size` the damage tracker uses, which is the scaled resolution.
    mode_size: Size<i32, Physical>,
    /// Cursor plane handle, when the surface has one and the driver is atomic.
    cursor_plane: Option<plane::Handle>,
    /// Image the cursor plane is scanning out. Held across vblanks for the same
    /// reason as `current`: releasing it early would free a buffer still on screen.
    cursor_fb: Option<CursorFb>,
    cursor_prev_fb: Option<CursorFb>,
    /// Where the plane puts the current image. Kept so a modeset can restate the
    /// plane without the main thread re-sending the position.
    cursor_dst: Rectangle<i32, Physical>,
}

impl Committer {
    pub(super) fn new(
        surface: Arc<DrmSurface>,
        mode_size: Size<i32, Physical>,
        cursor_plane: Option<plane::Handle>,
    ) -> Self {
        Self {
            surface,
            pending: None,
            current: None,
            needs_modeset: true,
            paused: false,
            mode_size,
            cursor_plane,
            cursor_fb: None,
            cursor_prev_fb: None,
            cursor_dst: Rectangle::default(),
        }
    }

    /// Stop issuing flips: the session was paused and DRM master is gone. Drops
    /// the in-flight buffers (their vblanks will never come) so the swapchain can
    /// reuse them once rendering resumes.
    pub fn pause(&mut self) {
        self.paused = true;
        self.pending = None;
        self.current = None;
        // The plane is reprogrammed from scratch on resume; holding the image
        // across the pause would leave a ghost pointer on the console.
        self.cursor_fb = None;
        self.cursor_prev_fb = None;
    }

    /// Resume after the session is active again. The next flip must modeset, since
    /// the surface state was reset on the main thread across the pause.
    pub fn resume(&mut self) {
        self.paused = false;
        self.needs_modeset = true;
    }

    /// Page-flip `fb` on the surface, or modeset on the first frame / after a VT
    /// switch. On success the matching vblank must later be acknowledged with
    /// `frame_submitted`; the swapchain slot backing `fb` is handed in so it is
    /// held until that vblank. Returns whether a commit was actually issued —
    /// `false` means no vblank event will come for this call.
    pub fn flip(&mut self, plan: FlipPlan) -> Result<bool, DrmError> {
        if self.paused {
            // No DRM master while paused; flipping would fail and corrupt state.
            // Drop the slot (no vblank will come for it) and report success so the
            // caller doesn't treat it as a flip error.
            drop(plan);
            return Ok(false);
        }

        let FlipPlan { primary, cursor } = plan;
        let mut planes: Vec<PlaneState<'_>> = Vec::with_capacity(2);

        if let Some((fb, _)) = &primary {
            let src = Rectangle::from_size(
                Size::<i32, BufferCoord>::from((self.mode_size.w, self.mode_size.h)),
            )
            .to_f64();
            let dst = Rectangle::from_size(self.mode_size);

            // No IN_FENCE_FD: it glitched on this i915. The render's fence is
            // exported for the frame pacer instead, not waited on the commit path.
            planes.push(PlaneState {
                handle: self.surface.plane(),
                config: Some(PlaneConfig {
                    src,
                    dst,
                    transform: Transform::Normal,
                    alpha: 1.0,
                    damage_clips: None,
                    fb: *fb,
                    fence: None,
                }),
            });
        }

        // A modeset rebuilds the whole CRTC state, so the cursor plane has to be
        // restated even when it did not change — otherwise it comes back blank.
        let modeset = self.needs_modeset || self.surface.commit_pending();
        let cursor = match (cursor, modeset, &self.cursor_fb) {
            (Some(update), _, _) => Some(update),
            (None, true, Some(_)) => Some(CursorUpdate::Move {
                dst: self.cursor_dst,
            }),
            _ => None,
        };

        if let (Some(plane), Some(update)) = (self.cursor_plane, cursor) {
            match update {
                CursorUpdate::Set { fb, dst } => {
                    self.cursor_prev_fb = self.cursor_fb.take();
                    self.cursor_dst = dst;
                    self.cursor_fb = Some(fb);
                }
                CursorUpdate::Move { dst } => self.cursor_dst = dst,
                CursorUpdate::Hide => {
                    self.cursor_prev_fb = self.cursor_fb.take();
                }
            }

            let config = self.cursor_fb.as_ref().map(|fb| {
                let src = Rectangle::from_size(Size::<i32, BufferCoord>::from((
                    self.cursor_dst.size.w,
                    self.cursor_dst.size.h,
                )))
                .to_f64();
                PlaneConfig {
                    src,
                    dst: self.cursor_dst,
                    transform: Transform::Normal,
                    alpha: 1.0,
                    damage_clips: None,
                    fb: fb.handle,
                    fence: None,
                }
            });
            planes.push(PlaneState { handle: plane, config });
        }

        if planes.is_empty() {
            return Ok(false);
        }

        if modeset {
            // Bracket the blocking modeset commit so a stall inside the ioctl
            // shows up as a start line with no matching done line.
            let crtc = self.surface.crtc();
            tracing::info!("modeset commit on {crtc:?}");
            self.surface.commit(planes, true)?;
            tracing::info!("modeset commit done on {crtc:?}");
            self.needs_modeset = false;
        } else {
            self.surface.page_flip(planes, true)?;
        }
        if let Some((_, slot)) = primary {
            self.pending = Some(slot);
        }
        Ok(true)
    }

    /// Acknowledge the vblank: the pending buffer is now the one being scanned
    /// out, so it becomes `current` (still held). The previous `current` is no
    /// longer displayed and is released back to the swapchain here — never
    /// earlier, or it would be reused mid-scanout and tear.
    pub fn frame_submitted(&mut self) {
        if self.pending.is_some() {
            self.current = self.pending.take();
        }
        self.cursor_prev_fb = None;
    }

    /// Re-sync surface state after a VT switch / session resume and force the
    /// next commit to modeset.
    pub fn reset_state(&mut self) -> Result<(), DrmError> {
        self.surface.reset_state()?;
        self.pending = None;
        self.current = None;
        self.needs_modeset = true;
        Ok(())
    }
}