//! Textures and render elements for the compositor's own chrome, plus the
//! per-surface render data. `element` wraps textures into damage-tracked
//! render elements, `graphics` owns the shared chrome assets (cursors, snap
//! preview, gesture handle), `surface_data` the per-wl_surface state.

mod element;
mod graphics;
mod surface_data;

pub use element::{render_elements_offscreen, ForgeElement, RenderTexture};
pub use graphics::{resize_icon, Graphics};
pub use surface_data::ForgeSurfaceData;

use std::cell::Cell;
use std::collections::HashMap;
use std::ops::Deref;
use std::rc::Rc;
use std::time::Instant;

use cursor_icon::CursorIcon;
use forge_ipc::WindowScale;
use smithay::backend::renderer::element::utils::{
    CropRenderElement, Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::memory::MemoryBuffer;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{
    ffi, GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform,
};
use smithay::backend::renderer::sync::SyncPoint;
use smithay::backend::renderer::utils::{
    draw_render_elements, Buffer, CommitCounter, DamageBag, DamageSet, DamageSnapshot, OpaqueRegions,
};
use smithay::backend::renderer::{self, Bind, Frame, Offscreen, Renderer, Texture as _};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{
    Buffer as BufferSpace, Logical, Physical, Point, Rectangle, Scale, Size, Transform,
};
use smithay::wayland::compositor::{
    BufferAssignment, Damage as SurfaceDamage, RectangleKind, SurfaceAttributes, SurfaceData,
};
use smithay::wayland::viewporter::{self, ViewportCachedState};

use crate::geometry::SubtractRectFast;
use crate::output::{Canvas, GESTURE_HANDLE_HEIGHT};

#[derive(Clone, Debug)]
pub struct Texture {
    opaque_regions: Vec<Rectangle<i32, Logical>>,
    tracker: DamageSnapshot<i32, BufferSpace>,
    location: Cell<Point<i32, Logical>>,
    src_rect: Rectangle<f64, Logical>,
    dst_size: Size<i32, Logical>,
    buffer_size: Size<i32, Logical>,
    buffer: Option<Buffer>,
    mem: Option<MemoryBuffer>,
    renderable: Renderable,
    transform: Transform,
    window_scale: Option<WindowScale>,
    scale: f64,
    id: Id,
}

impl Texture {
    pub fn new(
        texture: GlesTexture,
        buffer_size: impl Into<Size<i32, Logical>>,
        scale: f64,
        opaque: bool,
    ) -> Self {
        let buffer_size = buffer_size.into();
        let src_rect = Rectangle::from_size(buffer_size);
        let opaque_regions = opaque.then(|| vec![src_rect]).unwrap_or_default();

        Self {
            opaque_regions,
            buffer_size,
            scale,
            renderable: Renderable::Texture(texture),
            tracker: DamageSnapshot::empty(),
            src_rect: src_rect.to_f64(),
            dst_size: buffer_size,
            id: Id::new(),
            window_scale: None,
            transform: Transform::default(),
            location: Cell::new(Point::default()),
            buffer: None,
            mem: None,
        }
    }

    pub fn from_surface(
        texture: GlesTexture,
        window_scale: Option<WindowScale>,
        location: impl Into<Point<i32, Logical>>,
        buffer: &ForgeSurfaceData,
        surface: &WlSurface,
    ) -> Self {
        let mut opaque_regions =
            buffer
                .opaque_region
                .iter()
                .fold(Vec::new(), |mut acc, (kind, rect)| {
                    match kind {
                        RectangleKind::Add => acc.push(*rect),
                        RectangleKind::Subtract => acc.subtract_rect(*rect),
                    }
                    acc
                });

        // Clients painting rounded CSD corners routinely still declare the
        // whole surface opaque (Chromium); inside an opaque region the texture
        // is drawn with blending disabled and nothing is rendered beneath it,
        // so those corners come out solid. Free the corner squares of the
        // declared region so they always blend.
        if let Some(bounds) = opaque_regions.iter().copied().reduce(Rectangle::merge) {
            const CORNER: i32 = 16;
            let (w, h) = (bounds.size.w, bounds.size.h);
            if w > 2 * CORNER && h > 2 * CORNER {
                for (dx, dy) in [
                    (0, 0),
                    (w - CORNER, 0),
                    (0, h - CORNER),
                    (w - CORNER, h - CORNER),
                ] {
                    opaque_regions.subtract_rect(Rectangle::new(
                        (bounds.loc.x + dx, bounds.loc.y + dy).into(),
                        (CORNER, CORNER).into(),
                    ));
                }
            }
        }

        Self {
            opaque_regions,
            window_scale,
            tracker: buffer.damage.tracker.snapshot(),
            renderable: Renderable::Texture(texture),
            buffer_size: buffer.buffer_size,
            location: Cell::new(location.into()),
            buffer: buffer.buffer.clone(),
            transform: buffer.transform,
            scale: buffer.scale as f64,
            src_rect: buffer.src_rect,
            dst_size: buffer.dst_size,
            id: surface.into(),
            mem: None,
        }
    }

    pub fn from_spb(
        color: [u8; 4],
        window_scale: Option<WindowScale>,
        location: impl Into<Point<i32, Logical>>,
        buffer: &ForgeSurfaceData,
        surface: &WlSurface,
    ) -> Self {
        let opaque_regions = (color[3] == 255)
            .then(|| vec![Rectangle::from_size(buffer.dst_size)])
            .unwrap_or_default();

        Self {
            opaque_regions,
            window_scale,
            tracker: buffer.damage.tracker.snapshot(),
            renderable: Renderable::Color(color),
            buffer_size: buffer.buffer_size,
            location: Cell::new(location.into()),
            buffer: buffer.buffer.clone(),
            transform: buffer.transform,
            scale: buffer.scale as f64,
            src_rect: buffer.src_rect,
            dst_size: buffer.dst_size,
            id: surface.into(),
            mem: None,
        }
    }

    pub fn from_buffer(
        renderer: &mut GlesRenderer,
        scale: f64,
        buffer: &[u8],
        width: i32,
        height: i32,
        opaque: bool,
    ) -> Self {
        let texture = create_gl_texture(renderer, buffer, width, height, opaque);
        let logical_size = Size::<i32, Physical>::from((width, height))
            .to_f64()
            .to_logical(scale)
            .to_i32_round();
        Self::new(texture, logical_size, scale, opaque)
    }

    /// Like [`from_buffer`] but also retains the source pixels as a
    /// [`MemoryBuffer`], the CPU-accessible storage the cursor plane copies from.
    ///
    /// `buffer` is XCursor file order — a little-endian ARGB `u32`, so B,G,R,A in
    /// memory, which is what `Argb8888` names. The GLES upload reads RGBA instead,
    /// hence the swizzled copy.
    pub fn from_cursor_buffer(
        renderer: &mut GlesRenderer,
        scale: f64,
        buffer: &[u8],
        width: i32,
        height: i32,
    ) -> Self {
        let mut rgba = buffer.to_vec();
        for px in rgba.chunks_exact_mut(4) {
            px.swap(0, 2);
        }
        let mut texture = Self::from_buffer(renderer, scale, &rgba, width, height, false);
        texture.mem = Some(MemoryBuffer::from_slice(
            buffer,
            Fourcc::Argb8888,
            (width, height),
        ));
        texture
    }

    pub fn from_color(color: [u8; 4], opaque: bool) -> Self {
        let buffer_size = Size::from((1, 1));
        let src_rect = Rectangle::from_size(buffer_size);
        let opaque_regions = opaque.then(|| vec![src_rect]).unwrap_or_default();

        Self {
            opaque_regions,
            buffer_size,
            renderable: Renderable::Color(color),
            tracker: DamageSnapshot::empty(),
            src_rect: src_rect.to_f64(),
            dst_size: buffer_size,
            id: Id::new(),
            scale: 1.0,
            window_scale: None,
            transform: Transform::default(),
            location: Cell::new(Point::default()),
            buffer: None,
            mem: None,
        }
    }

    pub fn set_location(&self, location: Point<i32, Logical>) {
        self.location.set(location);
    }

    pub fn offset_location(&self, offset: Point<i32, Logical>) {
        self.location.set(self.location.get() + offset);
    }

    pub fn buffer_size(&self) -> Size<i32, Buffer> {
        match &self.renderable {
            Renderable::Texture(t) => (t.width() as i32, t.height() as i32).into(),
            Renderable::Color(_) => (1, 1).into(),
        }
    }

    pub fn size(&self) -> Size<i32, Logical> {
        self.dst_size
    }

    /// True when the surface declared a non-empty opaque region — i.e. it
    /// carries actual opaque content rather than being a transparent overlay
    /// (CSD shadow, input-region surface). Used to disambiguate the thumbnail
    /// pick when a client commits several same-size surfaces.
    pub fn is_opaque(&self) -> bool {
        !self.opaque_regions.is_empty()
    }
}

#[derive(Clone, Debug)]
enum Renderable {
    Texture(GlesTexture),
    Color([u8; 4]),
}

fn create_gl_texture(
    renderer: &mut GlesRenderer,
    buffer: &[u8],
    width: i32,
    height: i32,
    opaque: bool,
) -> GlesTexture {
    debug_assert!(buffer.len() as i32 >= width * height * 4);

    let texture_id = renderer
        .with_context(|gl| unsafe {
            let mut tex = 0;
            gl.GenTextures(1, &mut tex);
            gl.BindTexture(ffi::TEXTURE_2D, tex);
            gl.TexParameteri(
                ffi::TEXTURE_2D,
                ffi::TEXTURE_WRAP_S,
                ffi::CLAMP_TO_EDGE as i32,
            );
            gl.TexParameteri(
                ffi::TEXTURE_2D,
                ffi::TEXTURE_WRAP_T,
                ffi::CLAMP_TO_EDGE as i32,
            );
            gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MIN_FILTER, ffi::LINEAR as i32);
            gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MAG_FILTER, ffi::LINEAR as i32);
            gl.TexImage2D(
                ffi::TEXTURE_2D,
                0,
                ffi::RGBA as i32,
                width,
                height,
                0,
                ffi::RGBA,
                ffi::UNSIGNED_BYTE,
                buffer.as_ptr().cast(),
            );
            gl.BindTexture(ffi::TEXTURE_2D, 0);
            tex
        })
        .expect("texture creation");

    unsafe {
        GlesTexture::from_raw(
            renderer,
            Some(ffi::RGBA),
            opaque,
            texture_id,
            (width, height).into(),
        )
    }
}
