//! The hardware cursor plane: claim, image upload, and the `Presenter`'s
//! plane-vs-composited decision.

use super::*;

/// A cursor image uploaded to a scanout-capable buffer. Held by the `Committer`
/// for as long as the plane scans it out.
pub struct CursorFb {
    pub(super) handle: framebuffer::Handle,
    _buffer: GbmBuffer,
    _fb: GbmFramebuffer,
}

/// What the cursor plane shows on the next commit. Absent from a `FlipPlan`
/// when nothing about the cursor changed, so a scene-only frame does not
/// re-upload it.
pub enum CursorUpdate {
    /// New image, at this destination.
    Set {
        fb: CursorFb,
        dst: Rectangle<i32, Physical>,
    },
    /// Same image the plane already holds, moved.
    Move { dst: Rectangle<i32, Physical> },
    /// Take the cursor off the plane.
    Hide,
}

/// Main-thread half of the cursor plane: allocates and exports the small
/// scanout buffers, and remembers what the plane is already showing so an
/// unchanged frame costs nothing.
pub(super) struct CursorPlane {
    pub(super) handle: plane::Handle,
    pub(super) _claim: PlaneClaim,
    pub(super) allocator: GbmAllocator<DrmDeviceFd>,
    pub(super) exporter: Exporter,
    /// Fixed plane dimensions; an image larger than this cannot be scanned out.
    pub(super) size: Size<i32, BufferCoord>,
    pub(super) last_id: Option<Id>,
    pub(super) last_dst: Option<Rectangle<i32, Physical>>,
}
impl Presenter {
    /// Whether `el` can be scanned out by the cursor plane. A plane is a fixed
    /// size — 64×64 on most hardware — so a pointer larger than that (scale 3 and
    /// up) stays in the primary.
    pub(super) fn cursor_fits(&self, el: &ForgeElement) -> bool {
        if !self.cursor_enabled {
            return false;
        }
        let Some(plane) = &self.cursor else { return false };
        let geo = el.geometry(self.damage_scale.into());
        geo.size.w <= plane.size.w && geo.size.h <= plane.size.h
    }

    /// Take the pointer off the plane, if it is showing one. Used when there is
    /// no cursor this frame and when one cannot be scanned out.
    pub(super) fn hide_cursor_plane(&mut self) -> Option<CursorUpdate> {
        let plane = self.cursor.as_mut()?;
        plane.last_id.take()?;
        plane.last_dst = None;
        Some(CursorUpdate::Hide)
    }

    /// Work out what the cursor plane needs for `el`: a new image, a move, or
    /// nothing (the inner `None`).
    ///
    /// The outer `None` means the plane cannot show this element at all and the
    /// caller must leave it in the composed scene — a format the plane does not
    /// take, a buffer that is neither CPU memory nor shm, or an element the
    /// layout is scaling, which the hardware would not reproduce.
    pub(super) fn plan_cursor(
        &mut self,
        renderer: &mut GlesRenderer,
        el: &ForgeElement,
    ) -> Option<Option<CursorUpdate>> {
        let scale = self.damage_scale;
        let plane = self.cursor.as_mut()?;

        // The plane always shows a full cursor-sized buffer with the image in its
        // top-left corner, so the destination carries the plane's size, not the
        // image's — anything else would ask the hardware to scale.
        let geo = el.geometry(scale.into());
        let dst = Rectangle::new(geo.loc, Size::from((plane.size.w, plane.size.h)));
        let id = el.id().clone();
        if plane.last_id.as_ref() == Some(&id) {
            if plane.last_dst == Some(dst) {
                return Some(None);
            }
            plane.last_dst = Some(dst);
            return Some(Some(CursorUpdate::Move { dst }));
        }

        // The copy is one-to-one, so anything the element is cropping, scaling or
        // transforming has to stay in the scene where the renderer can do it.
        let src = el.src();
        if src.loc != Point::default()
            || src.size.w != geo.size.w as f64
            || src.size.h != geo.size.h as f64
            || el.transform() != Transform::Normal
        {
            return None;
        }

        // Shape changed: the pixels have to reach a scanout buffer. A themed
        // cursor keeps a CPU-side copy (`Texture::from_cursor_buffer`); one a
        // client attached as a surface is read straight out of its shm pool.
        let surface = &self.surface;
        let fb = match el.underlying_storage(renderer)? {
            UnderlyingStorage::Memory(mem) => {
                if mem.format() != Fourcc::Argb8888 {
                    return None;
                }
                plane.upload(surface, &mem, mem.stride() as usize, mem.size().h as usize)?
            }
            UnderlyingStorage::Wayland(buffer) => {
                shm::with_buffer_contents(buffer, |ptr, len, data| {
                    if shm::shm_format_to_fourcc(data.format) != Some(Fourcc::Argb8888) {
                        return None;
                    }
                    let expected = (data.stride * data.height) as usize;
                    if data.offset as usize + expected > len {
                        return None;
                    }
                    let src = unsafe {
                        std::slice::from_raw_parts(ptr.offset(data.offset as isize), expected)
                    };
                    plane.upload(surface, src, data.stride as usize, data.height as usize)
                })
                .ok()
                .flatten()?
            }
        };

        plane.last_id = Some(id);
        plane.last_dst = Some(dst);
        Some(Some(CursorUpdate::Set { fb, dst }))
    }}

impl CursorPlane {
    /// Copy `src` into a fresh cursor-sized scanout buffer and export a
    /// framebuffer for it. The image lands in the top-left corner and the rest
    /// stays transparent, which is why the plane's destination is always the
    /// full plane size.
    fn upload(
        &mut self,
        surface: &DrmSurface,
        src: &[u8],
        src_stride: usize,
        src_height: usize,
    ) -> Option<CursorFb> {
        let size = self.size;
        let mut buffer = self
            .allocator
            .create_buffer_with_flags(
                size.w as u32,
                size.h as u32,
                Fourcc::Argb8888,
                &[Modifier::Linear],
                GbmBufferFlags::CURSOR | GbmBufferFlags::WRITE,
            )
            .ok()?;

        let dst_stride = buffer.stride() as usize;
        let copied = buffer
            .map_mut(0, 0, size.w as u32, size.h as u32, |mapped| {
                let dst = mapped.buffer_mut();
                dst.fill(0);
                let row_bytes = src_stride.min(dst_stride);
                for row in 0..src_height.min(size.h as usize) {
                    let s = row * src_stride;
                    let d = row * dst_stride;
                    if s + row_bytes > src.len() || d + row_bytes > dst.len() {
                        break;
                    }
                    dst[d..d + row_bytes].copy_from_slice(&src[s..s + row_bytes]);
                }
            })
            .is_ok();
        if !copied {
            return None;
        }

        let gbm_fb = self
            .exporter
            .add_framebuffer(
                surface.device_fd(),
                ExportBuffer::Allocator(&buffer),
                false,
            )
            .ok()??;
        let handle = *gbm_fb.as_ref();
        Some(CursorFb {
            handle,
            _buffer: buffer,
            _fb: gbm_fb,
        })
    }
}