//! Per-output presentation: swapchain + render on the main thread, KMS commit on
//! a dedicated thread. Replaces smithay's `DrmCompositor` (which fused render and
//! commit in one `&mut self`).
//!
//! `Presenter` (main thread) owns the swapchain, GLES-facing pieces and damage
//! tracker; `render` composes the scene onto a free swapchain buffer and
//! `prepare_flip` does the swapchain bookkeeping. `Committer` (KMS thread) owns
//! the page-flip and the in-flight buffers across their vblanks. Every
//! `Committer` field is `Send`, so it crosses to the KMS thread while the `!Send`
//! renderer state stays on the main thread. `Presenter::new` returns both halves;
//! they share the `DrmSurface` through an `Arc` (it is `Sync`), but only the
//! `Committer` ever mutates it (flip / modeset / reset).

use std::os::fd::OwnedFd;
use std::sync::Arc;

use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf};
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBuffer};
use smithay::backend::allocator::{Fourcc, Modifier, Slot, Swapchain};
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
use smithay::backend::drm::exporter::ExportBuffer;
use smithay::backend::drm::exporter::ExportFramebuffer;
use smithay::backend::drm::gbm::GbmFramebuffer;
use smithay::backend::drm::{DrmDeviceFd, DrmError, DrmSurface, PlaneConfig, PlaneState};
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::RenderElementStates;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::{Bind, Color32F};
use smithay::reexports::drm::control::framebuffer;
use smithay::utils::{Physical, Rectangle, Size, Transform};

use crate::drawing::ForgeElement;

type Allocator0 = GbmAllocator<DrmDeviceFd>;
type Exporter = GbmFramebufferExporter<DrmDeviceFd>;

/// Exported state for a swapchain slot, cached in the slot's userdata so it is
/// created once per buffer and reused on every render/flip.
struct SlotExport {
    fb: framebuffer::Handle,
    _gbm_fb: GbmFramebuffer,
    dmabuf: Dmabuf,
}

/// Result of a render pass: the buffer to flip and whether it changed.
pub struct Rendered {
    slot: Slot<GbmBuffer>,
    fb: framebuffer::Handle,
    damaged: bool,
    /// Exported fence for this render, fed to the frame pacer to time the next
    /// frame. `None` when the renderer can't export a sync fd.
    pub gpu_fence: Option<OwnedFd>,
    /// Per-element render states, used to drive client frame callbacks
    /// (`Windows::mark_presented`).
    pub states: RenderElementStates,
}

/// 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>,
}

impl Committer {
    fn new(surface: Arc<DrmSurface>, mode_size: Size<i32, Physical>) -> Self {
        Self {
            surface,
            pending: None,
            current: None,
            needs_modeset: true,
            paused: false,
            mode_size,
        }
    }

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

    /// 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.
    pub fn flip(&mut self, fb: framebuffer::Handle, slot: Slot<GbmBuffer>) -> Result<(), 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(slot);
            return Ok(());
        }
        let plane = self.surface.plane();
        let src = Rectangle::from_size(
            Size::<i32, smithay::utils::Buffer>::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.
        let plane_state = PlaneState {
            handle: plane,
            config: Some(PlaneConfig {
                src,
                dst,
                transform: Transform::Normal,
                alpha: 1.0,
                damage_clips: None,
                fb,
                fence: None,
            }),
        };

        let modeset = self.needs_modeset || self.surface.commit_pending();
        if modeset {
            self.surface.commit([plane_state], true)?;
            self.needs_modeset = false;
        } else {
            self.surface.page_flip([plane_state], true)?;
        }
        self.pending = Some(slot);
        Ok(())
    }

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

    /// 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(())
    }
}

pub struct Presenter {
    /// Shared with the `Committer` on the KMS thread. The `Presenter` only reads
    /// it (crtc, plane info, device fd, commit_pending); the `Committer` mutates.
    surface: Arc<DrmSurface>,
    swapchain: Swapchain<Allocator0>,
    exporter: Exporter,
    damage: OutputDamageTracker,
    use_opaque: bool,
    /// Dmabuf of the last buffer composed this frame, exposed for the DMA
    /// capture paths (screencopy / export-dmabuf / image-copy) which used to
    /// read smithay's RenderFrameResult.
    last_dmabuf: Option<Dmabuf>,
    /// Mirrors the `Committer`'s modeset state for the main-thread skip decision:
    /// a frame must be flipped (not skipped) when damaged or when a modeset is
    /// still pending. Set on construction and on VT-switch reset; cleared when a
    /// flip is dispatched. The `Committer` keeps the authoritative copy for the
    /// commit-vs-page_flip choice; the next damaged frame resyncs the two.
    needs_modeset: bool,
    /// When true, the next render passes age=0 to the damage tracker so it does
    /// a full repaint, without touching the swapchain slots.  Set instead of
    /// calling `swapchain.reset_buffer_ages()`, which would leave count=1 slots
    /// in the Committer with open fds that never close.
    force_full_repaint: bool,
    /// CRTC mode size in real pixels — the blit destination rect for `blit_flip`.
    mode_size: Size<i32, Physical>,
}

impl Presenter {
    /// Build a presenter for one already-configured `DrmSurface`. `code` +
    /// `modifiers` must be a format the renderer can draw to and the plane can
    /// scan out (same selection smithay's DrmCompositor does internally).
    /// Returns the main-thread half and the KMS-thread `Committer`.
    pub fn new(
        surface: DrmSurface,
        allocator: Allocator0,
        exporter: Exporter,
        code: Fourcc,
        modifiers: Vec<Modifier>,
        size: Size<i32, Physical>,
        scale: f64,
    ) -> (Self, Committer) {
        let mode = surface.pending_mode();
        let mode_size = Size::<i32, Physical>::from((mode.size().0 as i32, mode.size().1 as i32));
        let swapchain = Swapchain::new(
            allocator,
            mode.size().0 as u32,
            mode.size().1 as u32,
            code,
            modifiers,
        );
        let use_opaque = !surface
            .plane_info()
            .formats
            .iter()
            .any(|f| f.code == code);
        let damage = OutputDamageTracker::new(size, scale, Transform::Normal);
        let surface = Arc::new(surface);
        let committer = Committer::new(surface.clone(), mode_size);
        let presenter = Self {
            surface,
            swapchain,
            exporter,
            damage,
            use_opaque,
            last_dmabuf: None,
            needs_modeset: true,
            force_full_repaint: true,
            mode_size,
        };
        (presenter, committer)
    }

    /// The dmabuf composed in the most recent `render`, for DMA captures.
    pub fn last_dmabuf(&self) -> Option<&Dmabuf> {
        self.last_dmabuf.as_ref()
    }

    pub fn crtc(&self) -> smithay::reexports::drm::control::crtc::Handle {
        self.surface.crtc()
    }

    pub fn surface(&self) -> &DrmSurface {
        &self.surface
    }

    /// 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> {
        let slot = match self.swapchain.acquire() {
            Ok(Some(slot)) => slot,
            Ok(None) => 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,
            elements,
            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 })
            }
            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)) => slot,
            Ok(None) => 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<(framebuffer::Handle, Slot<GbmBuffer>)> {
        let Rendered { slot, fb, damaged, gpu_fence: _, states: _ } = rendered;
        if !damaged && !self.needs_modeset {
            return None;
        }
        self.swapchain.submitted(&slot);
        self.needs_modeset = false;
        Some((fb, slot))
    }

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

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