//! Damage-tracked render elements over [`Texture`]: the shared-`Rc` handle,
//! the alpha wrapper, the composed `ForgeElement` stack and the offscreen
//! render helper.

use super::*;

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

impl RenderTexture {
    /// A clone of this solid-color texture sized to `size`. Keeps the same
    /// `Id`, so the damage tracker sees one element whose geometry changed
    /// rather than a new element every frame. The ceil overshoot from the
    /// physical→logical conversion is trimmed by the crop wrapper.
    fn sized_fill(&self, size: Size<i32, Logical>) -> Self {
        if self.0.dst_size == size {
            return self.clone();
        }
        let mut t = Texture::clone(&self.0);
        t.dst_size = size;
        if !t.opaque_regions.is_empty() {
            t.opaque_regions = vec![Rectangle::from_size(size)];
        }
        Self(Rc::new(t))
    }

    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,
        ));

        // Damage must round OUTWARD: nearest-rounding can fall 1px short of
        // the pixels a fractional buffer→output scale ratio actually touched,
        // leaving stale slivers at the edges of updated content.
        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_up(effective_scale)
        });

        DamageSet::from_iter(transformed)
    }

    fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
        // Opaque claims must round INWARD: over-claiming by a rounded pixel
        // culls a 1px strip of whatever renders beneath.
        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_down(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();

        // Solid-color fills are 1×1 textures; stretch them to the slot so
        // their geometry — and with it the damage their appearance produces —
        // covers the painted rect instead of a single pixel.
        let texture = match texture.0.renderable {
            Renderable::Color(_) => {
                let logical = Size::from((
                    (bounds.size.w as f64 / output_scale).ceil() as i32,
                    (bounds.size.h as f64 / output_scale).ceil() as i32,
                ));
                texture.sized_fill(logical)
            }
            _ => texture,
        };

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


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