use binrw::BinRead;

/// On-disk size of a single [`VertexElement`] entry in bytes.
pub const VERTEX_ELEMENT_SIZE: usize = 8;

/// Maximum number of [`VertexElement`] slots per declaration block, including the sentinel.
pub const VERTEX_ELEMENT_SLOTS: usize = 17;

/// Total on-disk byte size of one vertex declaration block (17 slots × 8 bytes).
pub const VERTEX_DECLARATION_SLOT_BYTES: usize = VERTEX_ELEMENT_SLOTS * VERTEX_ELEMENT_SIZE;

pub(crate) const STREAM_SENTINEL: u8 = 0xFF;

/// Semantic role of a vertex element within a declaration.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VertexUsage {
    /// XYZ (and optionally W) position data.
    Position,
    /// Skinning blend weights.
    BlendWeights,
    /// Skinning blend indices into the bone palette.
    BlendIndices,
    /// Surface normal.
    Normal,
    /// Texture coordinates.
    UV,
    /// Tangent vector.
    Tangent,
    /// Bitangent (binormal) vector.
    Bitangent,
    /// Vertex colour.
    Color,
    /// An unrecognised usage discriminant.
    Unknown(u8),
}

impl VertexUsage {
    /// Converts the on-disk `u8` discriminant to a [`VertexUsage`].
    #[must_use]
    pub fn from_raw(raw: u8) -> Self {
        match raw {
            0 => VertexUsage::Position,
            1 => VertexUsage::BlendWeights,
            2 => VertexUsage::BlendIndices,
            3 => VertexUsage::Normal,
            4 => VertexUsage::UV,
            5 => VertexUsage::Tangent,
            6 => VertexUsage::Bitangent,
            7 => VertexUsage::Color,
            other => VertexUsage::Unknown(other),
        }
    }

    /// Returns the on-disk `u8` discriminant for this usage.
    #[must_use]
    pub fn as_raw(self) -> u8 {
        match self {
            VertexUsage::Position => 0,
            VertexUsage::BlendWeights => 1,
            VertexUsage::BlendIndices => 2,
            VertexUsage::Normal => 3,
            VertexUsage::UV => 4,
            VertexUsage::Tangent => 5,
            VertexUsage::Bitangent => 6,
            VertexUsage::Color => 7,
            VertexUsage::Unknown(raw) => raw,
        }
    }
}

/// A single vertex attribute within a [`VertexDeclaration`].
///
/// Describes where in the vertex stream(s) the attribute lives and how it should be
/// interpreted.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VertexElement {
    /// Zero-based vertex stream index (0–2).
    pub stream: u8,
    /// Byte offset of this attribute within a single vertex.
    pub offset: u8,
    /// Packed data-type discriminant (format, component count, normalisation).
    pub data_type: u8,
    /// Semantic role of this element.
    pub usage: VertexUsage,
    /// Disambiguates multiple elements with the same usage (e.g. UV0 vs UV1).
    pub usage_index: u8,
}

#[derive(Debug, BinRead, Clone, Copy)]
#[br(little)]
pub(crate) struct WireVertexElement {
    pub stream: u8,
    pub offset: u8,
    pub data_type: u8,
    pub usage: u8,
    pub usage_index: u8,
    pub _padding: [u8; 3],
}

impl WireVertexElement {
    #[must_use]
    pub fn is_sentinel(self) -> bool {
        self.stream == STREAM_SENTINEL
    }
}

impl From<WireVertexElement> for VertexElement {
    fn from(w: WireVertexElement) -> Self {
        VertexElement {
            stream: w.stream,
            offset: w.offset,
            data_type: w.data_type,
            usage: VertexUsage::from_raw(w.usage),
            usage_index: w.usage_index,
        }
    }
}

/// Vertex buffer layout for one mesh — a list of [`VertexElement`] descriptors.
///
/// Each declaration occupies exactly [`VERTEX_DECLARATION_SLOT_BYTES`] on disk; up to
/// [`VERTEX_ELEMENT_SLOTS`] − 1 real elements may be present before the 0xFF sentinel.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VertexDeclaration {
    /// The decoded vertex elements, in the order they appear on disk (sentinel excluded).
    pub elements: Vec<VertexElement>,
}

impl VertexDeclaration {
    /// Returns the fixed on-disk byte size of a declaration block, [`VERTEX_DECLARATION_SLOT_BYTES`].
    #[must_use]
    pub fn on_disk_byte_size() -> usize {
        VERTEX_DECLARATION_SLOT_BYTES
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn vertex_usage_round_trip() {
        for raw in 0..=7u8 {
            assert_eq!(VertexUsage::from_raw(raw).as_raw(), raw);
        }
    }

    #[test]
    fn vertex_usage_unknown_round_trip() {
        let u = VertexUsage::from_raw(99);
        assert_eq!(u, VertexUsage::Unknown(99));
        assert_eq!(u.as_raw(), 99);
    }

    #[test]
    fn declaration_slot_size_is_136() {
        assert_eq!(VERTEX_DECLARATION_SLOT_BYTES, 136);
        assert_eq!(VERTEX_ELEMENT_SIZE, 8);
        assert_eq!(VERTEX_ELEMENT_SLOTS, 17);
    }
}
