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

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

const CURSOR_FILL: [u8; 4] = [255, 255, 255, 255];
const CURSOR_BORDER: [u8; 4] = [0, 0, 0, 255];

const GESTURE_HANDLE_DEFAULT_RGB: [u8; 3] = [255; 3];

const GESTURE_NOTCH_RATIO: f64 = 0.2;
const CURSOR_SIZE: f64 = 32.0;

#[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 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
                });

        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`] so the element exposes CPU-accessible storage.
    /// Smithay's DrmCompositor needs that to copy the element into the
    /// cursor plane's buffer; a GLES-only texture is skipped and composited
    /// into the primary instead. The format is declared `Argb8888` to match
    /// the cursor BO; the cursor glyphs are greyscale, so the RGBA byte order
    /// of `buffer` is identical under that format.
    pub fn from_cursor_buffer(
        renderer: &mut GlesRenderer,
        scale: f64,
        buffer: &[u8],
        width: i32,
        height: i32,
    ) -> Self {
        let mut texture = Self::from_buffer(renderer, scale, buffer, 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]),
}

#[derive(Clone, Debug)]
pub struct RenderTexture(Rc<Texture>);

impl RenderTexture {
    pub fn new(texture: Texture) -> Self {
        Self(Rc::new(texture))
    }
}

impl Deref for RenderTexture {
    type Target = Texture;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl Element for RenderTexture {
    fn id(&self) -> &Id {
        &self.id
    }

    fn current_commit(&self) -> CommitCounter {
        self.tracker.current_commit()
    }

    fn src(&self) -> Rectangle<f64, BufferSpace> {
        self.src_rect
            .to_buffer(self.scale, self.transform, &self.buffer_size.to_f64())
    }

    fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
        let effective_scale = self.window_scale.map_or(scale.x, |ws| ws.scale(scale.x));
        Rectangle::new(self.location.get(), self.dst_size)
            .to_physical_precise_round(effective_scale)
    }

    fn damage_since(
        &self,
        scale: Scale<f64>,
        commit: Option<CommitCounter>,
    ) -> DamageSet<i32, Physical> {
        let geometry = self.geometry(scale);

        let damage = match self.tracker.damage_since(commit) {
            Some(d) => d,
            None => return DamageSet::from_slice(&[Rectangle::from_size(geometry.size)]),
        };

        let effective_scale = self.window_scale.map_or(scale.x, |ws| ws.scale(scale.x));

        let buffer_size = self
            .buffer_size
            .to_f64()
            .to_buffer(self.scale, self.transform);
        let viewporter_scale = Scale::from((
            self.dst_size.w as f64 / self.src_rect.size.w,
            self.dst_size.h as f64 / self.src_rect.size.h,
        ));

        let transformed = damage.into_iter().map(|rect| {
            let mut logical = rect
                .to_f64()
                .to_logical(self.scale, self.transform, &buffer_size)
                .upscale(viewporter_scale);
            logical.loc -= self.src_rect.loc;
            logical.to_physical_precise_round(effective_scale)
        });

        DamageSet::from_iter(transformed)
    }

    fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
        let effective_scale = self.window_scale.map_or(scale.x, |ws| ws.scale(scale.x));
        self.opaque_regions
            .iter()
            .map(|r| r.to_physical_precise_round(effective_scale))
            .collect()
    }
}

impl RenderElement<GlesRenderer> for RenderTexture {
    fn draw<'a>(
        &self,
        frame: &mut GlesFrame,
        src: Rectangle<f64, BufferSpace>,
        dst: Rectangle<i32, Physical>,
        damage: &[Rectangle<i32, Physical>],
        opaque_regions: &[Rectangle<i32, Physical>],
    ) -> Result<(), GlesError> {
        match &self.renderable {
            Renderable::Texture(texture) => frame.render_texture_from_to(
                texture,
                src,
                dst,
                damage,
                opaque_regions,
                self.transform,
                1.0,
                None,
                &[],
            ),
            Renderable::Color([r, g, b, a]) => {
                let color = [
                    *r as f32 / 255.0,
                    *g as f32 / 255.0,
                    *b as f32 / 255.0,
                    *a as f32 / 255.0,
                ];
                frame.draw_solid(dst, damage, color.into())
            }
        }
    }

    fn underlying_storage(&self, _: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
        if let Some(mem) = self.mem.as_ref() {
            return Some(UnderlyingStorage::Memory(mem));
        }
        self.buffer.as_ref().map(UnderlyingStorage::Wayland)
    }
}

/// Wraps a [`RenderTexture`] with an alpha multiplier applied at draw
/// time. Sits BETWEEN the smithay layout wrappers (Rescale / Relocate /
/// Crop) and the leaf texture so that `render_texture_from_to` actually
/// receives the alpha — `RenderTexture::draw` hard-codes `1.0`, so
/// declaring an `Element::alpha()` override at the outer layers would
/// otherwise be silently dropped by `draw_render_elements`.
#[derive(Debug, Clone)]
struct AlphaTexture {
    texture: RenderTexture,
    alpha: f32,
    program: Option<GlesTexProgram>,
    uniforms: Vec<Uniform<'static>>,
}

impl Element for AlphaTexture {
    fn id(&self) -> &Id {
        self.texture.id()
    }
    fn current_commit(&self) -> CommitCounter {
        self.texture.current_commit()
    }
    fn src(&self) -> Rectangle<f64, BufferSpace> {
        self.texture.src()
    }
    fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
        self.texture.geometry(scale)
    }
    fn damage_since(
        &self,
        scale: Scale<f64>,
        commit: Option<CommitCounter>,
    ) -> DamageSet<i32, Physical> {
        self.texture.damage_since(scale, commit)
    }
    fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
        if self.program.is_some() {
            OpaqueRegions::default()
        } else if self.alpha >= 0.999 {
            self.texture.opaque_regions(scale)
        } else {
            OpaqueRegions::default()
        }
    }
    fn alpha(&self) -> f32 {
        self.alpha
    }
}

impl RenderElement<GlesRenderer> for AlphaTexture {
    fn draw<'a>(
        &self,
        frame: &mut GlesFrame,
        src: Rectangle<f64, BufferSpace>,
        dst: Rectangle<i32, Physical>,
        damage: &[Rectangle<i32, Physical>],
        opaque_regions: &[Rectangle<i32, Physical>],
    ) -> Result<(), GlesError> {
        match &self.texture.renderable {
            Renderable::Texture(t) => frame.render_texture_from_to(
                t,
                src,
                dst,
                damage,
                opaque_regions,
                self.texture.transform,
                self.alpha,
                self.program.as_ref(),
                &self.uniforms,
            ),
            Renderable::Color([r, g, b, a]) => {
                let color = [
                    *r as f32 / 255.0,
                    *g as f32 / 255.0,
                    *b as f32 / 255.0,
                    *a as f32 / 255.0 * self.alpha,
                ];
                frame.draw_solid(dst, damage, color.into())
            }
        }
    }

    fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
        self.texture.underlying_storage(renderer)
    }
}

#[derive(Debug)]
pub struct ForgeElement(
    RescaleRenderElement<
        CropRenderElement<RelocateRenderElement<RescaleRenderElement<AlphaTexture>>>,
    >,
    Kind,
);

impl ForgeElement {
    pub fn add_element(
        elements: &mut Vec<Self>,
        texture: RenderTexture,
        location: impl Into<Point<i32, Physical>>,
        bounds: Rectangle<i32, Physical>,
        window_scale: impl Into<Option<f64>>,
        output_scale: f64,
    ) {
        Self::add_element_panel(
            elements,
            texture,
            location,
            bounds,
            window_scale,
            output_scale,
            1.0,
            1.0,
            (0, 0).into(),
        );
    }

    /// Like `add_element` but applies a uniform alpha and an optional
    /// scale around `panel_center`. Used by the app switcher to grow the
    /// whole panel from a slightly smaller size during fade-in. Pass
    /// `alpha = 1.0`, `panel_scale = 1.0` and `panel_center = (0, 0)` for
    /// a no-op outer rescale.
    pub fn add_element_panel(
        elements: &mut Vec<Self>,
        texture: RenderTexture,
        location: impl Into<Point<i32, Physical>>,
        bounds: Rectangle<i32, Physical>,
        window_scale: impl Into<Option<f64>>,
        output_scale: f64,
        alpha: f32,
        panel_scale: f64,
        panel_center: Point<i32, Physical>,
    ) {
        Self::add_element_panel_kind(
            elements,
            texture,
            location,
            bounds,
            window_scale,
            output_scale,
            alpha,
            panel_scale,
            panel_center,
            Kind::ScanoutCandidate,
        );
    }

    pub fn add_element_panel_composited(
        elements: &mut Vec<Self>,
        texture: RenderTexture,
        location: impl Into<Point<i32, Physical>>,
        bounds: Rectangle<i32, Physical>,
        window_scale: impl Into<Option<f64>>,
        output_scale: f64,
        alpha: f32,
        panel_scale: f64,
        panel_center: Point<i32, Physical>,
    ) {
        Self::add_element_panel_kind(
            elements,
            texture,
            location,
            bounds,
            window_scale,
            output_scale,
            alpha,
            panel_scale,
            panel_center,
            Kind::Unspecified,
        );
    }

    fn add_element_panel_kind(
        elements: &mut Vec<Self>,
        texture: RenderTexture,
        location: impl Into<Point<i32, Physical>>,
        bounds: Rectangle<i32, Physical>,
        window_scale: impl Into<Option<f64>>,
        output_scale: f64,
        alpha: f32,
        panel_scale: f64,
        panel_center: Point<i32, Physical>,
        kind: Kind,
    ) {
        let window_scale = window_scale.into().unwrap_or(1.0);
        let location = location.into();

        let alpha_tex = AlphaTexture {
            texture,
            alpha,
            program: None,
            uniforms: Vec::new(),
        };
        let rescaled = RescaleRenderElement::from_element(alpha_tex, (0, 0).into(), window_scale);
        let relocated = RelocateRenderElement::from_element(rescaled, location, Relocate::Relative);

        if let Some(cropped) = CropRenderElement::from_element(relocated, output_scale, bounds) {
            let outer = RescaleRenderElement::from_element(cropped, panel_center, panel_scale);
            elements.push(Self(outer, kind));
        }
    }

    /// Add the cursor texture as an element flagged for the hardware cursor
    /// plane (`Kind::Cursor`). Smithay's DrmCompositor routes elements with
    /// this kind to the CRTC's cursor plane, so a mouse move becomes a
    /// minimal atomic commit (CRTC_X/Y) instead of a full scene re-render.
    pub fn add_cursor(
        elements: &mut Vec<Self>,
        texture: RenderTexture,
        location: impl Into<Point<i32, Physical>>,
        bounds: Rectangle<i32, Physical>,
        window_scale: impl Into<Option<f64>>,
        output_scale: f64,
    ) {
        let window_scale = window_scale.into().unwrap_or(1.0);
        let location = location.into();

        let alpha_tex = AlphaTexture {
            texture,
            alpha: 1.0,
            program: None,
            uniforms: Vec::new(),
        };
        let rescaled = RescaleRenderElement::from_element(alpha_tex, (0, 0).into(), window_scale);
        let relocated = RelocateRenderElement::from_element(rescaled, location, Relocate::Relative);

        if let Some(cropped) = CropRenderElement::from_element(relocated, output_scale, bounds) {
            let outer = RescaleRenderElement::from_element(cropped, (0, 0).into(), 1.0);
            elements.push(Self(outer, Kind::Cursor));
        }
    }

    /// Like [`add_element`] but rounds the texture's two bottom corners
    /// with `radius` (logical px) via the custom `program`. Used for the
    /// server-decorated window body so the bottom of the window matches
    /// the rounded top of its SSD bar.
    pub fn add_element_rounded(
        elements: &mut Vec<Self>,
        texture: RenderTexture,
        location: impl Into<Point<i32, Physical>>,
        bounds: Rectangle<i32, Physical>,
        window_scale: impl Into<Option<f64>>,
        output_scale: f64,
        program: GlesTexProgram,
        radius: f32,
    ) {
        let window_scale = window_scale.into().unwrap_or(1.0);
        let location = location.into();

        let size = texture.size();
        let uniforms = vec![
            Uniform::new("surface_size", [size.w as f32, size.h as f32]),
            Uniform::new("corner_radius", radius),
        ];

        let alpha_tex = AlphaTexture {
            texture,
            alpha: 1.0,
            program: Some(program),
            uniforms,
        };
        let rescaled = RescaleRenderElement::from_element(alpha_tex, (0, 0).into(), window_scale);
        let relocated = RelocateRenderElement::from_element(rescaled, location, Relocate::Relative);

        if let Some(cropped) = CropRenderElement::from_element(relocated, output_scale, bounds) {
            let outer = RescaleRenderElement::from_element(cropped, (0, 0).into(), 1.0);
            elements.push(Self(outer, Kind::ScanoutCandidate));
        }
    }
}

impl Element for ForgeElement {
    fn id(&self) -> &Id {
        self.0.id()
    }
    fn current_commit(&self) -> CommitCounter {
        self.0.current_commit()
    }
    fn src(&self) -> Rectangle<f64, BufferSpace> {
        self.0.src()
    }
    fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
        self.0.geometry(scale)
    }
    fn damage_since(
        &self,
        scale: Scale<f64>,
        commit: Option<CommitCounter>,
    ) -> DamageSet<i32, Physical> {
        self.0.damage_since(scale, commit)
    }
    fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
        self.0.opaque_regions(scale)
    }
    fn alpha(&self) -> f32 {
        self.0.alpha()
    }
    fn kind(&self) -> Kind {
        self.1
    }
}

impl RenderElement<GlesRenderer> for ForgeElement {
    fn draw<'a>(
        &self,
        frame: &mut GlesFrame,
        src: Rectangle<f64, BufferSpace>,
        dst: Rectangle<i32, Physical>,
        damage: &[Rectangle<i32, Physical>],
        opaque_regions: &[Rectangle<i32, Physical>],
    ) -> Result<(), GlesError> {
        self.0.draw(frame, src, dst, damage, opaque_regions)
    }

    fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
        self.0.underlying_storage(renderer)
    }
}

#[derive(Debug)]
pub struct Graphics {
    gesture_handle_default: Option<RenderTexture>,
    cursor: Option<RenderTexture>,
    /// Cached resize cursors, indexed by `ResizeCursorKind`.
    /// 0: NS, 1: EW, 2: NWSE, 3: NESW
    resize_cursors: [Option<RenderTexture>; 4],

    /// XCursor theme loaded from the active ltk theme; the source for
    /// `cursor_for`. Built-in vector cursors are the fallback.
    cursor_theme: crate::cursor::CursorTheme,
    /// Cached themed cursor textures keyed by shape and quantised scale.
    themed_cursors: HashMap<(CursorIcon, i32), CachedCursor>,

    /// Solid backdrop drawn under every `View::Lock(_)` frame. The
    /// `ext-session-lock-v1` spec requires the compositor to keep the
    /// session locked even after the lock client dies and to render a
    /// solid fallback in the meantime so the frozen desktop never
    /// leaks through. Cached as an opaque black 1×1 texture; the GLES
    /// blitter scales it across the full output.
    pub lock_fallback: RenderTexture,

    /// Translucent accent-coloured rectangle used as the visual
    /// preview while a window is being drag-snapped to a screen
    /// edge or corner. Rendered on top of every other surface so the
    /// user sees the target zone before they release. Premultiplied
    /// alpha at upload time so the cached buffer is ready for the
    /// `Texture::from_color(_, true)` path.
    pub snap_preview: RenderTexture,
}

impl Graphics {
    pub fn new() -> Self {
        Self {
            gesture_handle_default: None,
            cursor: None,
            resize_cursors: [None, None, None, None],
            cursor_theme: crate::cursor::CursorTheme::load(),
            themed_cursors: HashMap::new(),
            lock_fallback: RenderTexture::new(Texture::from_color([0, 0, 0, 255], false)),
            // Eydolon teal accent (#00CEB1) at ~35 % alpha, premultiplied:
            //   r' = 0x00 * 0x59 / 0xff ≈ 0
            //   g' = 0xCE * 0x59 / 0xff ≈ 0x47
            //   b' = 0xB1 * 0x59 / 0xff ≈ 0x3D
            //   a  = 0x59 (≈ 35 %)
            snap_preview: RenderTexture::new(Texture::from_color([0x00, 0x47, 0x3D, 0x59], true)),
        }
    }

    pub fn gesture_handle(
        &mut self,
        renderer: &mut GlesRenderer,
        canvas: &Canvas,
    ) -> RenderTexture {
        let slot = &mut self.gesture_handle_default;
        let color = GESTURE_HANDLE_DEFAULT_RGB;

        let scale = canvas.scale();
        let width = canvas.physical_size().w;
        let height = (GESTURE_HANDLE_HEIGHT as f64 * scale).round() as i32;
        let expected_size: Size<i32, Buffer> = (width, height).into();

        if slot
            .as_ref()
            .is_none_or(|h| h.buffer_size() != expected_size)
        {
            let buffer = generate_gesture_handle_buffer(width, height, &color);
            let texture = Texture::from_buffer(renderer, scale, &buffer, width, height, true);
            *slot = Some(RenderTexture::new(texture));
        }

        slot.as_ref()
            .expect("gesture handle texture just initialised")
            .clone()
    }

    pub fn cursor(&mut self, renderer: &mut GlesRenderer, canvas: &Canvas) -> RenderTexture {
        let scale = canvas.scale();
        let size = (CURSOR_SIZE * scale).round() as i32;
        let expected_size: Size<i32, Buffer> = (size, size).into();

        if self
            .cursor
            .as_ref()
            .is_none_or(|c| c.buffer_size() != expected_size)
        {
            let buffer = generate_cursor_buffer(size);
            let texture = Texture::from_cursor_buffer(renderer, scale, &buffer, size, size);
            self.cursor = Some(RenderTexture::new(texture));
        }

        self.cursor
            .as_ref()
            .expect("cursor texture just initialised")
            .clone()
    }

    pub fn resize_cursor(
        &mut self,
        renderer: &mut GlesRenderer,
        canvas: &Canvas,
        kind: ResizeCursorKind,
    ) -> RenderTexture {
        let scale = canvas.scale();
        let size = (CURSOR_SIZE * scale).round() as i32;
        let expected_size: Size<i32, Buffer> = (size, size).into();
        let slot = &mut self.resize_cursors[kind as usize];

        if slot
            .as_ref()
            .is_none_or(|c| c.buffer_size() != expected_size)
        {
            let buffer = generate_resize_cursor_buffer(size, kind);
            let texture = Texture::from_cursor_buffer(renderer, scale, &buffer, size, size);
            *slot = Some(RenderTexture::new(texture));
        }

        slot.as_ref()
            .expect("resize cursor texture just initialised")
            .clone()
    }

    /// Returns the texture and hotspot (in physical pixels) for `icon`,
    /// taken from the loaded XCursor theme. Falls back to the built-in
    /// vector cursors when the theme has no matching shape.
    pub fn cursor_for(
        &mut self,
        renderer: &mut GlesRenderer,
        canvas: &Canvas,
        icon: CursorIcon,
    ) -> (RenderTexture, Point<i32, Physical>) {
        let scale = canvas.scale();
        let key = (icon, (scale * 100.0).round() as i32);
        if let Some(cached) = self.themed_cursors.get(&key) {
            return (cached.texture.clone(), cached.hotspot);
        }

        if let Some(img) = self.cursor_theme.image(icon, scale) {
            let texture = RenderTexture::new(Texture::from_cursor_buffer(
                renderer, scale, &img.pixels, img.width, img.height,
            ));
            let hotspot = Point::from((img.xhot, img.yhot));
            self.themed_cursors.insert(
                key,
                CachedCursor {
                    texture: texture.clone(),
                    hotspot,
                },
            );
            return (texture, hotspot);
        }

        if let Some(kind) = resize_kind(icon) {
            let texture = self.resize_cursor(renderer, canvas, kind);
            let half = (CURSOR_SIZE * scale).round() as i32 / 2;
            (texture, Point::from((half, half)))
        } else {
            (self.cursor(renderer, canvas), Point::from((0, 0)))
        }
    }
}

#[derive(Debug)]
struct CachedCursor {
    texture: RenderTexture,
    hotspot: Point<i32, Physical>,
}

/// Maps an SSD resize edge to the cursor shape forge requests for it.
pub fn resize_icon(edge: crate::ssd::ResizeEdge) -> CursorIcon {
    match ResizeCursorKind::from(edge) {
        ResizeCursorKind::NS => CursorIcon::NsResize,
        ResizeCursorKind::EW => CursorIcon::EwResize,
        ResizeCursorKind::NWSE => CursorIcon::NwseResize,
        ResizeCursorKind::NESW => CursorIcon::NeswResize,
    }
}

/// The built-in resize glyph that best matches a themed resize shape, used
/// only as the fallback when the theme is missing.
fn resize_kind(icon: CursorIcon) -> Option<ResizeCursorKind> {
    match icon {
        CursorIcon::NsResize | CursorIcon::NResize | CursorIcon::SResize | CursorIcon::RowResize => {
            Some(ResizeCursorKind::NS)
        }
        CursorIcon::EwResize | CursorIcon::EResize | CursorIcon::WResize | CursorIcon::ColResize => {
            Some(ResizeCursorKind::EW)
        }
        CursorIcon::NwseResize | CursorIcon::NwResize | CursorIcon::SeResize => {
            Some(ResizeCursorKind::NWSE)
        }
        CursorIcon::NeswResize | CursorIcon::NeResize | CursorIcon::SwResize => {
            Some(ResizeCursorKind::NESW)
        }
        _ => None,
    }
}

/// Which resize cursor glyph to draw. `ResizeEdge` collapses to one of
/// these four shapes (N==S, E==W, NE==SW, NW==SE).
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ResizeCursorKind {
    NS = 0,
    EW = 1,
    NWSE = 2,
    NESW = 3,
}

impl From<crate::ssd::ResizeEdge> for ResizeCursorKind {
    fn from(edge: crate::ssd::ResizeEdge) -> Self {
        use crate::ssd::ResizeEdge::*;
        match edge {
            N | S => ResizeCursorKind::NS,
            E | W => ResizeCursorKind::EW,
            NW | SE => ResizeCursorKind::NWSE,
            NE | SW => ResizeCursorKind::NESW,
        }
    }
}

/// Draws a double-ended arrow (line + two chevrons) into an `size × size`
/// RGBA8888 buffer. All glyphs are centered so the hotspot is the texture
/// middle.
fn generate_resize_cursor_buffer(size: i32, kind: ResizeCursorKind) -> Vec<u8> {
    let mut buf = vec![0u8; (size * size * 4) as usize];
    let s = size as f64;
    let cx = s * 0.5;
    let cy = s * 0.5;
    let half_len = s * 0.36; // distance from centre to tip of chevron
    let chev = s * 0.12; // chevron leg length
    let thickness = (s * 0.06).max(1.5);
    let border = thickness * 1.6;

    // Two endpoints in logical (x, y) space.
    let (p0, p1) = match kind {
        ResizeCursorKind::EW => ((cx - half_len, cy), (cx + half_len, cy)),
        ResizeCursorKind::NS => ((cx, cy - half_len), (cx, cy + half_len)),
        ResizeCursorKind::NWSE => (
            (cx - half_len * 0.70, cy - half_len * 0.70),
            (cx + half_len * 0.70, cy + half_len * 0.70),
        ),
        ResizeCursorKind::NESW => (
            (cx + half_len * 0.70, cy - half_len * 0.70),
            (cx - half_len * 0.70, cy + half_len * 0.70),
        ),
    };

    // Direction along shaft (unit).
    let dx = p1.0 - p0.0;
    let dy = p1.1 - p0.1;
    let len = (dx * dx + dy * dy).sqrt().max(1e-6);
    let ux = dx / len;
    let uy = dy / len;
    // Perpendicular.
    let px = -uy;
    let py = ux;

    // Chevron tip-back points for each endpoint (inward along shaft).
    let chev_segments: [((f64, f64), (f64, f64)); 4] = [
        // p0 chevron: two legs going back and out.
        (
            p0,
            (p0.0 + ux * chev + px * chev, p0.1 + uy * chev + py * chev),
        ),
        (
            p0,
            (p0.0 + ux * chev - px * chev, p0.1 + uy * chev - py * chev),
        ),
        // p1 chevron: two legs going back and out.
        (
            p1,
            (p1.0 - ux * chev + px * chev, p1.1 - uy * chev + py * chev),
        ),
        (
            p1,
            (p1.0 - ux * chev - px * chev, p1.1 - uy * chev - py * chev),
        ),
    ];

    let segments: [(f64, f64, f64, f64); 5] = [
        (p0.0, p0.1, p1.0, p1.1),
        (
            chev_segments[0].0 .0,
            chev_segments[0].0 .1,
            chev_segments[0].1 .0,
            chev_segments[0].1 .1,
        ),
        (
            chev_segments[1].0 .0,
            chev_segments[1].0 .1,
            chev_segments[1].1 .0,
            chev_segments[1].1 .1,
        ),
        (
            chev_segments[2].0 .0,
            chev_segments[2].0 .1,
            chev_segments[2].1 .0,
            chev_segments[2].1 .1,
        ),
        (
            chev_segments[3].0 .0,
            chev_segments[3].0 .1,
            chev_segments[3].1 .0,
            chev_segments[3].1 .1,
        ),
    ];

    for y in 0..size {
        for x in 0..size {
            let fx = x as f64 + 0.5;
            let fy = y as f64 + 0.5;
            let mut min_d = f64::MAX;
            for (ax, ay, bx, by) in &segments {
                let d = segment_distance(fx, fy, *ax, *ay, *bx, *by);
                if d < min_d {
                    min_d = d;
                }
            }
            let off = ((y * size + x) * 4) as usize;
            if min_d <= thickness {
                buf[off..off + 4].copy_from_slice(&CURSOR_FILL);
            } else if min_d <= border {
                buf[off..off + 4].copy_from_slice(&CURSOR_BORDER);
            }
        }
    }

    buf
}

fn segment_distance(px: f64, py: f64, ax: f64, ay: f64, bx: f64, by: f64) -> f64 {
    let dx = bx - ax;
    let dy = by - ay;
    let len_sq = dx * dx + dy * dy;
    if len_sq < 1e-6 {
        return ((px - ax).powi(2) + (py - ay).powi(2)).sqrt();
    }
    let t = ((px - ax) * dx + (py - ay) * dy) / len_sq;
    let t = t.clamp(0.0, 1.0);
    let qx = ax + t * dx;
    let qy = ay + t * dy;
    ((px - qx).powi(2) + (py - qy).powi(2)).sqrt()
}

fn generate_gesture_handle_buffer(width: i32, height: i32, color: &[u8; 3]) -> Vec<u8> {
    let mut buffer = vec![0u8; (height * width * 4) as usize];

    let notch_h = (height as f64 * GESTURE_NOTCH_RATIO) as i32;
    let notch_w = (width as f64 * GESTURE_NOTCH_RATIO) as i32;

    let y_start = (height - notch_h) / 2;
    let y_end = (height + notch_h) / 2;
    let x_start = (width - notch_w) / 2;
    let x_end = (width + notch_w) / 2;

    for y in y_start..y_end {
        for x in x_start..x_end {
            let offset = ((y * width + x) * 4) as usize;
            buffer[offset..offset + 3].copy_from_slice(color);
        }
    }

    buffer
}

fn generate_cursor_buffer(size: i32) -> Vec<u8> {
    let mut buffer = vec![0u8; (size * size * 4) as usize];
    let s = size as f64;
    let border = (s * 0.09).max(1.0);

    // Arrow pointer vertices (normalized 0..1):
    //   tip (0,0), tail-left (0, 0.9), notch (0.3, 0.65), tail-right (0.65, 0.65)
    let verts: [(f64, f64); 4] = [(0.0, 0.0), (0.0, 0.9), (0.3, 0.65), (0.65, 0.65)];

    for y in 0..size {
        for x in 0..size {
            let fx = (x as f64 + 0.5) / s;
            let fy = (y as f64 + 0.5) / s;

            if point_in_arrow(fx, fy, &verts) {
                let offset = ((y * size + x) * 4) as usize;
                let dist = arrow_edge_dist(fx, fy, &verts) * s;
                if dist < border {
                    buffer[offset..offset + 4].copy_from_slice(&CURSOR_BORDER);
                } else {
                    buffer[offset..offset + 4].copy_from_slice(&CURSOR_FILL);
                }
            }
        }
    }

    buffer
}

/// Test if point (px,py) is inside the arrow polygon using ray casting.
fn point_in_arrow(px: f64, py: f64, verts: &[(f64, f64); 4]) -> bool {
    let n = verts.len();
    let mut inside = false;
    let mut j = n - 1;
    for i in 0..n {
        let (xi, yi) = verts[i];
        let (xj, yj) = verts[j];
        if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
            inside = !inside;
        }
        j = i;
    }
    inside
}

/// Minimum distance from point to any edge of the arrow polygon.
fn arrow_edge_dist(px: f64, py: f64, verts: &[(f64, f64); 4]) -> f64 {
    let n = verts.len();
    let mut min_dist = f64::MAX;
    let mut j = n - 1;
    for i in 0..n {
        let (x1, y1) = verts[j];
        let (x2, y2) = verts[i];
        let dx = x2 - x1;
        let dy = y2 - y1;
        let len_sq = dx * dx + dy * dy;
        let t = if len_sq > 0.0 {
            ((px - x1) * dx + (py - y1) * dy) / len_sq
        } else {
            0.0
        };
        let t = t.clamp(0.0, 1.0);
        let cx = x1 + t * dx;
        let cy = y1 + t * dy;
        let dist = ((px - cx).powi(2) + (py - cy).powi(2)).sqrt();
        if dist < min_dist {
            min_dist = dist;
        }
        j = i;
    }
    min_dist
}

pub struct ForgeSurfaceData {
    pub opaque_region: Vec<(RectangleKind, Rectangle<i32, Logical>)>,
    pub preferred_fractional_scale: f64,
    pub texture: Option<RenderTexture>,
    pub buffer_size: Size<i32, Logical>,
    pub src_rect: Rectangle<f64, Logical>,
    pub dst_size: Size<i32, Logical>,
    pub inhibits_shortcuts: bool,
    pub buffer: Option<Buffer>,
    pub transform: Transform,
    pub damage: Damage,
    pub scale: i32,
    pub location: Point<i32, Logical>,
}

impl Default for ForgeSurfaceData {
    fn default() -> Self {
        Self {
            preferred_fractional_scale: 1.0,
            scale: 1,
            inhibits_shortcuts: false,
            opaque_region: Vec::new(),
            buffer_size: Size::default(),
            transform: Transform::default(),
            src_rect: Rectangle::default(),
            dst_size: Size::default(),
            location: Point::default(),
            texture: None,
            buffer: None,
            damage: Damage::default(),
        }
    }
}

impl ForgeSurfaceData {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn update_buffer(
        &mut self,
        surface_data: &SurfaceData,
        attributes: &mut SurfaceAttributes,
        assignment: BufferAssignment,
    ) {
        match assignment {
            BufferAssignment::NewBuffer(buffer) => {
                let old_size = self.buffer_size;

                self.scale = attributes.buffer_scale;
                self.transform = attributes.buffer_transform.into();
                self.buffer_size = renderer::buffer_dimensions(&buffer)
                    .unwrap_or_default()
                    .to_logical(self.scale, self.transform);
                self.buffer = Some(Buffer::with_implicit(buffer));
                self.texture = None;

                let viewport_valid =
                    viewporter::ensure_viewport_valid(surface_data, self.buffer_size);
                let (viewport_src, viewport_dst) = viewport_valid
                    .then(|| {
                        let mut state = surface_data.cached_state.get::<ViewportCachedState>();
                        let current = state.current();
                        (current.src, current.size())
                    })
                    .unwrap_or_default();

                self.src_rect =
                    viewport_src.unwrap_or_else(|| Rectangle::from_size(self.buffer_size.to_f64()));
                self.dst_size = viewport_dst.unwrap_or(self.buffer_size);

                if old_size != self.buffer_size {
                    self.damage.tracker.reset();
                }
            }
            BufferAssignment::Removed => *self = Self::default(),
        }

        let viewporter_scale = Scale::from((
            self.dst_size.w as f64 / self.src_rect.size.w,
            self.dst_size.h as f64 / self.src_rect.size.h,
        ));

        let buffer_clip = self
            .buffer
            .as_ref()
            .and_then(|b| renderer::buffer_dimensions(b))
            .map(Rectangle::from_size)
            .unwrap_or_default();

        let buffer_damage = attributes.damage.drain(..).filter_map(|damage| {
            let rect = match damage {
                SurfaceDamage::Buffer(b) => b,
                SurfaceDamage::Surface(mut logical) => {
                    logical.loc += self.src_rect.loc.to_i32_round();
                    logical
                        .to_buffer(self.scale, self.transform, &self.buffer_size)
                        .to_f64()
                        .downscale(viewporter_scale)
                        .to_i32_round()
                }
            };

            let rect = rect.intersection(buffer_clip)?;
            self.damage.buffer.push(rect);
            Some(rect)
        });

        self.damage.tracker.add(buffer_damage);
    }
}

#[derive(Default)]
pub struct Damage {
    buffer: Vec<Rectangle<i32, BufferSpace>>,
    tracker: DamageBag<i32, BufferSpace>,
}

impl Damage {
    pub fn buffer(&self) -> &[Rectangle<i32, BufferSpace>] {
        &self.buffer
    }

    pub fn clear(&mut self) {
        self.buffer.clear();
    }
}

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

/// Render `elements` into a freshly-created offscreen target and return it with
/// the render sync point. `T` is the buffer kind: a `GlesTexture` to reuse the
/// result as a thumbnail, or a `GlesRenderbuffer` to read it back to SHM. Keeps
/// the create→bind→render→clear→draw→finish sequence in one place, shared by
/// the app-switcher snapshot and the screencopy path.
pub fn render_elements_offscreen<T>(
    renderer: &mut GlesRenderer,
    format: Fourcc,
    buffer_size: Size<i32, BufferSpace>,
    render_size: Size<i32, Physical>,
    scale: f64,
    transform: Transform,
    clear: [f32; 4],
    elements: &[ForgeElement],
    damage: &[Rectangle<i32, Physical>],
) -> Result<(T, SyncPoint), GlesError>
where
    GlesRenderer: Offscreen<T> + Bind<T>,
{
    let mut target: T = renderer.create_buffer(format, buffer_size)?;
    let sync = {
        let mut framebuffer = renderer.bind(&mut target)?;
        let mut frame = renderer.render(&mut framebuffer, render_size, transform)?;
        frame.clear(clear.into(), damage)?;
        draw_render_elements(&mut frame, scale, elements, damage)?;
        frame.finish()?
    };
    Ok((target, sync))
}
