// Copyright 2026 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Render targets, texture bindings, and coordinate-space mappings.

use vello_common::geometry::RectU16;

/// The root target provided by the user.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RootTarget {
    /// The root target is the user-provided surface.
    UserSurface,
    /// The root render target is an atlas layer.
    AtlasLayer,
}

/// The target for a draw pass.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DrawPassTarget {
    /// Draw directly to the root.
    Root(RootTarget),
    /// Draw to an intermediate layer texture.
    Layer(LayerTextureId),
}

impl DrawPassTarget {
    pub(crate) fn enable_opaque(&self) -> bool {
        // We could support other targets, but then we need to pay the cost of a depth buffer.
        matches!(self, Self::Root(RootTarget::UserSurface))
    }
}

/// Even or odd intermediate texture group.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum TextureParity {
    /// Texture group used by even layer depths.
    Even = 0,
    /// Texture group used by odd layer depths.
    Odd = 1,
}

impl TextureParity {
    pub(crate) const fn from_parity(parity: usize) -> Self {
        if parity & 1 == 0 {
            Self::Even
        } else {
            Self::Odd
        }
    }

    pub(crate) const fn get_parity(self) -> usize {
        match self {
            Self::Even => 0,
            Self::Odd => 1,
        }
    }

    pub(crate) const fn opposite(self) -> Self {
        match self {
            Self::Even => Self::Odd,
            Self::Odd => Self::Even,
        }
    }
}

impl From<TextureParity> for u8 {
    fn from(parity: TextureParity) -> Self {
        parity as Self
    }
}

impl From<TextureParity> for u32 {
    fn from(parity: TextureParity) -> Self {
        u8::from(parity).into()
    }
}

/// Identifies a page in an intermediate layer texture group.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct LayerTextureId {
    /// The parity of the texture.
    pub(crate) texture_parity: TextureParity,
    /// The page index of the texture within the parity group.
    pub(crate) page_index: u16,
}

impl LayerTextureId {
    pub(crate) const fn new(texture_parity: TextureParity, page_index: u16) -> Self {
        Self {
            texture_parity,
            page_index,
        }
    }
}

/// Output and optional child input used by a draw pass.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct DrawPassBindings {
    /// Texture or root surface receiving the draw.
    pub(crate) target: DrawPassTarget,
    /// Child layer sampled by the draw, if any.
    pub(crate) child: Option<LayerTextureId>,
}

impl DrawPassBindings {
    pub(crate) const fn new(target: DrawPassTarget, child: Option<LayerTextureId>) -> Self {
        Self { target, child }
    }
}

/// Target and temporary layer textures used by a filter pass.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct FilterPassBindings {
    /// Texture containing the original input and receiving the final result.
    target: LayerTextureId,
    /// Opposite-parity texture used for filter ping-ponging.
    temporary: LayerTextureId,
}

impl FilterPassBindings {
    pub(crate) const fn new(target: LayerTextureId, temporary: LayerTextureId) -> Self {
        debug_assert!(
            target.texture_parity as u8 != temporary.texture_parity as u8,
            "filter target and temporary textures must have opposite parity"
        );

        Self { target, temporary }
    }

    pub(crate) const fn target(self) -> LayerTextureId {
        self.target
    }

    pub(crate) const fn temporary(self) -> LayerTextureId {
        self.temporary
    }

    pub(crate) const fn input(self, step: usize) -> LayerTextureId {
        if step & 1 == 0 {
            self.target()
        } else {
            self.temporary()
        }
    }

    pub(crate) const fn output(self, step: usize) -> LayerTextureId {
        if step & 1 == 0 {
            self.temporary()
        } else {
            self.target()
        }
    }
}

/// Target and child layer textures used by a blend pass.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct BlendPassBindings {
    /// Layer serving as the blend backdrop and destination.
    target: LayerTextureId,
    /// Child layer serving as the blend source.
    child: LayerTextureId,
}

impl BlendPassBindings {
    pub(crate) const fn new(target: LayerTextureId, child: LayerTextureId) -> Self {
        debug_assert!(
            target.texture_parity as u8 != child.texture_parity as u8,
            "blend target and child textures must have opposite parity"
        );

        Self { target, child }
    }

    pub(crate) const fn target(self) -> LayerTextureId {
        self.target
    }

    pub(crate) const fn layer_id(self, parity: TextureParity) -> LayerTextureId {
        if self.target.texture_parity as u8 == parity as u8 {
            self.target
        } else {
            self.child
        }
    }
}

/// The bound layer textures for a round.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct RoundBindings {
    /// The bound page for each parity.
    pages: [Option<u16>; 2],
}

impl RoundBindings {
    pub(crate) const fn new(id: LayerTextureId) -> Self {
        let mut pages = [None; 2];

        pages[id.texture_parity.get_parity()] = Some(id.page_index);

        Self { pages }
    }

    /// Try to merge this binding with the other one.
    pub(crate) fn merge(mut self, other: Self) -> Option<Self> {
        for (current, required) in self.pages.iter_mut().zip(other.pages) {
            match (*current, required) {
                (Some(current), Some(required)) if current != required => return None,
                (None, Some(required)) => *current = Some(required),
                _ => {}
            }
        }

        Some(self)
    }

    /// Return the bound texture for a parity, if present.
    pub(crate) fn layer_id(self, parity: TextureParity) -> Option<LayerTextureId> {
        self.pages[parity.get_parity()].map(|page_index| LayerTextureId::new(parity, page_index))
    }

    /// Return the required textures for this binding.
    pub(crate) fn required_textures(self) -> [Option<LayerTextureId>; 2] {
        core::array::from_fn(|index| {
            self.pages[index].map(|page_index| {
                LayerTextureId::new(TextureParity::from_parity(index), page_index)
            })
        })
    }

    #[cfg(test)]
    pub(crate) const fn page_indices(self) -> [Option<u16>; 2] {
        self.pages
    }
}

/// Rectangular region within a render target.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct TextureRegion {
    /// Render target containing the region.
    pub(crate) target: LayerTextureId,
    /// Region in the layer texture.
    pub(crate) rect: RectU16,
}

/// Intermediate texture region paired with its scene-space layer bounds.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LayerTextureRegion {
    /// The texture region of the layer.
    pub(crate) texture: TextureRegion,
    /// Bounds of this layer in **viewport** coordinates.
    pub(crate) layer_bbox: RectU16,
}

impl LayerTextureRegion {
    /// Restrict this region to the given scene-space bounds while preserving its texture mapping.
    pub(crate) fn crop_to(self, bounds: RectU16) -> Self {
        let layer_bbox = self.layer_bbox.intersect(bounds);

        Self {
            texture: TextureRegion {
                target: self.texture.target,
                rect: self.texture_rect(layer_bbox),
            },
            layer_bbox,
        }
    }

    /// Translate a scene-space rectangle within this layer to texture coordinates.
    ///
    /// The given bbox must be fully contained within the layer bbox.
    pub(crate) fn texture_rect(self, bbox: RectU16) -> RectU16 {
        let x0 = self.texture.rect.x0 + (bbox.x0.checked_sub(self.layer_bbox.x0).unwrap());
        let y0 = self.texture.rect.y0 + (bbox.y0.checked_sub(self.layer_bbox.y0).unwrap());

        RectU16::new(x0, y0, x0 + bbox.width(), y0 + bbox.height())
    }
}

pub(crate) trait DrawTarget {
    /// Whether this target supports the depth-buffer optimization.
    fn enable_depth(&self) -> bool;

    /// A positional shift that needs to be applied to all geometry when rendering to this target.
    fn geometry_shift(&self) -> (i32, i32);
}

impl DrawTarget for RootTarget {
    fn enable_depth(&self) -> bool {
        matches!(self, Self::UserSurface)
    }

    fn geometry_shift(&self) -> (i32, i32) {
        (0, 0)
    }
}

impl DrawTarget for LayerTextureRegion {
    fn enable_depth(&self) -> bool {
        false
    }

    fn geometry_shift(&self) -> (i32, i32) {
        // We always render layers such that their bbox starts at (0, 0) in the allocated
        // texture region, to minimize the consumed space.
        (
            self.texture.rect.x0 as i32 - i32::from(self.layer_bbox.x0),
            self.texture.rect.y0 as i32 - i32::from(self.layer_bbox.y0),
        )
    }
}

#[cfg(test)]
mod tests {
    use super::{LayerTextureId, LayerTextureRegion, RoundBindings, TextureParity, TextureRegion};
    use vello_common::geometry::RectU16;

    #[test]
    fn texture_pair_constraints() {
        let constraint = |pages| RoundBindings { pages };
        let empty = constraint([None, None]);
        let even = constraint([Some(2), None]);
        let odd = constraint([None, Some(5)]);

        assert_eq!(even.merge(empty).unwrap().pages, [Some(2), None]);
        assert_eq!(empty.merge(even).unwrap().pages, [Some(2), None]);
        assert_eq!(even.merge(odd).unwrap().pages, [Some(2), Some(5)]);
        assert_eq!(even.merge(even).unwrap().pages, [Some(2), None]);
        assert!(even.merge(constraint([Some(3), None])).is_none());
        assert!(odd.merge(constraint([None, Some(6)])).is_none());
    }

    #[test]
    fn texture_rect_translation() {
        let layer = LayerTextureRegion {
            texture: TextureRegion {
                target: LayerTextureId::new(TextureParity::Even, 0),
                rect: RectU16::new(100, 200, 150, 250),
            },
            layer_bbox: RectU16::new(10, 20, 60, 70),
        };

        assert_eq!(
            layer.texture_rect(RectU16::new(15, 30, 25, 42)),
            RectU16::new(105, 210, 115, 222)
        );
    }

    #[test]
    fn crop_preserves_texture_mapping() {
        let layer = LayerTextureRegion {
            texture: TextureRegion {
                target: LayerTextureId::new(TextureParity::Even, 0),
                rect: RectU16::new(100, 200, 150, 250),
            },
            layer_bbox: RectU16::new(10, 20, 60, 70),
        };

        assert_eq!(
            layer.crop_to(RectU16::new(15, 30, 25, 42)),
            LayerTextureRegion {
                texture: TextureRegion {
                    target: layer.texture.target,
                    rect: RectU16::new(105, 210, 115, 222),
                },
                layer_bbox: RectU16::new(15, 30, 25, 42),
            }
        );
    }
}
