use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::gles::{GlesRenderbuffer, GlesRenderer, GlesTarget};
use smithay::backend::renderer::{Bind, ExportMem};
use smithay::utils::{Buffer as BufferSpace, Physical, Point, Rectangle, Size, Transform};

use crate::drawing::{render_elements_offscreen, ForgeElement};

/// Read `region` of the bound `target` as tightly-packed RGBA bytes, in GL row
/// order, through `ExportMem` — the path that makes the target's framebuffer
/// current. Raw `ReadPixels` after `bind` samples whatever framebuffer the
/// last compose left current instead (`bind` itself leaves nothing bound).
pub fn read_target_rgba(
    renderer: &mut GlesRenderer,
    target: &GlesTarget<'_>,
    region: Rectangle<i32, BufferSpace>,
) -> Result<Vec<u8>, String> {
    let mapping = renderer
        .copy_framebuffer(target, region, Fourcc::Abgr8888)
        .map_err(|e| format!("copy_framebuffer: {e:?}"))?;
    renderer
        .map_texture(&mapping)
        .map(<[u8]>::to_vec)
        .map_err(|e| format!("map_texture: {e:?}"))
}

/// Render `elements` (a full-output scene, in output-physical coordinates) into
/// an offscreen buffer, read back the `region` sub-rectangle and encode it as
/// PNG. The region is clamped to the output bounds.
pub fn encode_region_png(
    renderer: &mut GlesRenderer,
    output_size: Size<i32, Physical>,
    scale: f64,
    elements: &[ForgeElement],
    region: Rectangle<i32, Physical>,
) -> Result<Vec<u8>, String> {
    let ow = output_size.w;
    let oh = output_size.h;
    if ow <= 0 || oh <= 0 {
        return Err("zero-size output".into());
    }

    let rx = region.loc.x.clamp(0, ow);
    let ry = region.loc.y.clamp(0, oh);
    let rw = region.size.w.min(ow - rx);
    let rh = region.size.h.min(oh - ry);
    if rw <= 0 || rh <= 0 {
        return Err("empty region".into());
    }

    let buffer_size = Size::<i32, BufferSpace>::from((ow, oh));
    let full = Rectangle::<i32, Physical>::new(Point::from((0, 0)), output_size);
    let (mut rb, _sync): (GlesRenderbuffer, _) = render_elements_offscreen(
        renderer,
        Fourcc::Abgr8888,
        buffer_size,
        output_size,
        scale,
        Transform::Normal,
        [0.0, 0.0, 0.0, 1.0],
        elements,
        &[full],
    )
    .map_err(|e| format!("render offscreen: {e:?}"))?;

    // The offscreen render comes out top-down: read the region as-is.
    let target = renderer.bind(&mut rb).map_err(|e| format!("bind renderbuffer: {e:?}"))?;
    let read_region = Rectangle::<i32, BufferSpace>::new(
        Point::from((rx, ry)),
        Size::from((rw, rh)),
    );
    let pixels = read_target_rgba(renderer, &target, read_region)?;

    encode_rgba_png(rw, rh, pixels)
}

/// Read back the `region` sub-rectangle of the bound `target` (e.g. the
/// composited swapchain buffer) and encode it as PNG. The region is clamped
/// to the output bounds.
pub fn read_region_png(
    renderer: &mut GlesRenderer,
    target: &GlesTarget<'_>,
    output_size: Size<i32, Physical>,
    region: Rectangle<i32, Physical>,
) -> Result<Vec<u8>, String> {
    let ow = output_size.w;
    let oh = output_size.h;
    if ow <= 0 || oh <= 0 {
        return Err("zero-size output".into());
    }

    let rx = region.loc.x.clamp(0, ow);
    let ry = region.loc.y.clamp(0, oh);
    let rw = region.size.w.min(ow - rx);
    let rh = region.size.h.min(oh - ry);
    if rw <= 0 || rh <= 0 {
        return Err("empty region".into());
    }

    // The composited scanout buffer is stored top-down (GL-y 0 is the top row),
    // so read straight at `ry` and keep the row order — no GL bottom-up flip.
    let read_region =
        Rectangle::<i32, BufferSpace>::new(Point::from((rx, ry)), Size::from((rw, rh)));
    let pixels = read_target_rgba(renderer, target, read_region)?;

    encode_rgba_png(rw, rh, pixels)
}

/// Read back the whole bound `target` as tightly-packed RGBA rows, top-down
/// (scanout buffers store GL-y 0 as the top row, like `read_region_png`).
pub fn read_frame_rgba(
    renderer: &mut GlesRenderer,
    target: &GlesTarget<'_>,
    output_size: Size<i32, Physical>,
) -> Result<Vec<u8>, String> {
    let (w, h) = (output_size.w, output_size.h);
    if w <= 0 || h <= 0 {
        return Err("zero-size output".into());
    }
    read_target_rgba(
        renderer,
        target,
        Rectangle::<i32, BufferSpace>::from_size(Size::from((w, h))),
    )
}

fn encode_rgba_png(w: i32, h: i32, mut pixels: Vec<u8>) -> Result<Vec<u8>, String> {
    // Force opaque: the scanout buffer may carry alpha < 255, which a paste
    // target would flatten to white. A screenshot is always opaque.
    for px in pixels.chunks_exact_mut(4) {
        px[3] = 255;
    }
    let img = image::RgbaImage::from_raw(w as u32, h as u32, pixels)
        .ok_or("rgba buffer size mismatch")?;
    let mut out = Vec::new();
    img.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png)
        .map_err(|e| format!("png encode: {e}"))?;
    Ok(out)
}
