use binrw::BinRead;

/// On-disk size of the [`ModelFileHeader`] in bytes (0x44).
pub const MODEL_FILE_HEADER_SIZE: usize = 0x44;

/// On-disk size of the [`ModelHeader`] in bytes (0x38).
pub const MODEL_HEADER_SIZE: usize = 0x38;

/// First set of per-model rendering flags from [`ModelHeader`].
///
/// Controls occlusion, lighting, shadow, and animation features.
/// Use [`ModelFlags1::contains`] with the named constants.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ModelFlags1(u8);

impl ModelFlags1 {
    /// Dust occlusion effect is active.
    pub const DUST_OCCLUSION_ENABLED: u8 = 0x80;
    /// Snow occlusion effect is active.
    pub const SNOW_OCCLUSION_ENABLED: u8 = 0x40;
    /// Rain occlusion effect is active.
    pub const RAIN_OCCLUSION_ENABLED: u8 = 0x20;
    /// Unknown flag bit 4.
    pub const UNKNOWN1: u8 = 0x10;
    /// Lighting reflection effect is active.
    pub const LIGHTING_REFLECTION_ENABLED: u8 = 0x08;
    /// Waving (wind) animation is disabled.
    pub const WAVING_ANIMATION_DISABLED: u8 = 0x04;
    /// Light-shadow casting is disabled.
    pub const LIGHT_SHADOW_DISABLED: u8 = 0x02;
    /// Shadow casting is disabled entirely.
    pub const SHADOW_DISABLED: u8 = 0x01;

    /// Wraps a raw `u8` flags byte from the wire header.
    #[must_use]
    pub fn from_bits(bits: u8) -> Self {
        ModelFlags1(bits)
    }

    /// Returns the raw `u8` value.
    #[must_use]
    pub fn bits(self) -> u8 {
        self.0
    }

    /// Returns `true` if all bits in `mask` are set.
    #[must_use]
    pub fn contains(self, mask: u8) -> bool {
        self.0 & mask == mask
    }
}

/// Second set of per-model rendering flags from [`ModelHeader`].
///
/// Controls LOD, edge geometry, shadow masking, and background-specific features.
/// Use [`ModelFlags2::contains`] with the named constants.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ModelFlags2(u8);

impl ModelFlags2 {
    /// Unknown flag bit 7.
    pub const UNKNOWN2: u8 = 0x80;
    /// Background UV scrolling is enabled.
    pub const BG_UV_SCROLL_ENABLED: u8 = 0x40;
    /// Model may be forcibly made non-resident.
    pub const ENABLE_FORCE_NON_RESIDENT: u8 = 0x20;
    /// Extra LOD data (see [`ExtraLod`]) is present in the file.
    ///
    /// [`ExtraLod`]: crate::mdl::lod::ExtraLod
    pub const EXTRA_LOD_ENABLED: u8 = 0x10;
    /// Shadow mask is active.
    pub const SHADOW_MASK_ENABLED: u8 = 0x08;
    /// Force-LOD range override is active.
    pub const FORCE_LOD_RANGE_ENABLED: u8 = 0x04;
    /// Edge geometry is present.
    pub const EDGE_GEOMETRY_ENABLED: u8 = 0x02;
    /// Unknown flag bit 0.
    pub const UNKNOWN3: u8 = 0x01;

    /// Wraps a raw `u8` flags byte from the wire header.
    #[must_use]
    pub fn from_bits(bits: u8) -> Self {
        ModelFlags2(bits)
    }

    /// Returns the raw `u8` value.
    #[must_use]
    pub fn bits(self) -> u8 {
        self.0
    }

    /// Returns `true` if all bits in `mask` are set.
    #[must_use]
    pub fn contains(self, mask: u8) -> bool {
        self.0 & mask == mask
    }
}

/// First header of an MDL file — describes the file layout and buffer offsets.
///
/// This 0x44-byte structure is read at offset 0.  The [`ModelHeader`] follows after the
/// vertex-declaration block and string table.
///
/// Obtained from [`Mdl::file_header`].
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, BinRead)]
#[br(little)]
pub struct ModelFileHeader {
    /// Format version; `0x01000006` is the minimum that uses the Dawntrail bone-table layout.
    pub version: u32,
    /// Size in bytes of the runtime vertex/index stack region.
    pub stack_size: u32,
    /// Size in bytes of the runtime model data region.
    pub runtime_size: u32,
    /// Number of vertex declarations immediately following this header.
    pub vertex_declaration_count: u16,
    /// Number of materials referenced by this model.
    pub material_count: u16,
    /// Per-LOD byte offsets to the start of each vertex buffer.
    pub vertex_offset: [u32; 3],
    /// Per-LOD byte offsets to the start of each index buffer.
    pub index_offset: [u32; 3],
    /// Per-LOD byte sizes of the vertex buffers.
    pub vertex_buffer_size: [u32; 3],
    /// Per-LOD byte sizes of the index buffers.
    pub index_buffer_size: [u32; 3],
    /// Number of LODs in the file; typically 3.
    pub lod_count: u8,
    /// Whether index buffer streaming is enabled.
    pub enable_index_buffer_streaming: u8,
    /// Whether edge geometry is included.
    pub enable_edge_geometry: u8,
    #[allow(dead_code)]
    pub padding: u8,
}

/// Second, semantic header of an MDL file — holds counts and rendering parameters.
///
/// This 0x38-byte structure is read after the vertex declarations and string table.
///
/// Obtained from [`Mdl::model_header`].
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ModelHeader {
    /// Bounding sphere radius for the model.
    pub radius: f32,
    /// Total number of [`Mesh`] entries.
    ///
    /// [`Mesh`]: crate::mdl::mesh::Mesh
    pub mesh_count: u16,
    /// Number of attribute name offsets into the string table.
    pub attribute_count: u16,
    /// Total number of [`Submesh`] entries.
    ///
    /// [`Submesh`]: crate::mdl::mesh::Submesh
    pub submesh_count: u16,
    /// Number of material name offsets into the string table.
    pub material_count: u16,
    /// Number of bone name offsets into the string table.
    pub bone_count: u16,
    /// Number of [`BoneTable`] entries.
    ///
    /// [`BoneTable`]: crate::mdl::bone::BoneTable
    pub bone_table_count: u16,
    /// Number of [`Shape`] entries.
    ///
    /// [`Shape`]: crate::mdl::shape::Shape
    pub shape_count: u16,
    /// Number of [`ShapeMesh`] entries.
    ///
    /// [`ShapeMesh`]: crate::mdl::shape::ShapeMesh
    pub shape_mesh_count: u16,
    /// Number of [`ShapeValue`] entries.
    ///
    /// [`ShapeValue`]: crate::mdl::shape::ShapeValue
    pub shape_value_count: u16,
    /// Number of active LOD levels.
    pub lod_count: u8,
    /// First set of rendering flags; see [`ModelFlags1`].
    pub flags1: ModelFlags1,
    /// Number of [`ElementId`] entries.
    ///
    /// [`ElementId`]: crate::mdl::lod::ElementId
    pub element_id_count: u16,
    /// Number of terrain shadow meshes.
    pub terrain_shadow_mesh_count: u8,
    /// Second set of rendering flags; see [`ModelFlags2`].
    pub flags2: ModelFlags2,
    /// Distance beyond which the model is clipped out.
    pub model_clip_out_distance: f32,
    /// Distance beyond which shadows are clipped out.
    pub shadow_clip_out_distance: f32,
    /// Unknown `u16` field.
    pub unknown4: u16,
    /// Number of terrain shadow submeshes.
    pub terrain_shadow_submesh_count: u16,
    /// Unknown `u8` field.
    pub unknown5: u8,
    /// Index of the material used for background change effects.
    pub bg_change_material_index: u8,
    /// Index of the material used for background crest change effects.
    pub bg_crest_change_material_index: u8,
    /// Unknown `u8` field.
    pub unknown6: u8,
    /// Unknown `u16` field.
    pub unknown7: u16,
    /// Unknown `u16` field.
    pub unknown8: u16,
    /// Unknown `u16` field.
    pub unknown9: u16,
}

impl ModelHeader {
    /// Returns `true` if [`ExtraLod`] blocks are present in the file.
    ///
    /// [`ExtraLod`]: crate::mdl::lod::ExtraLod
    #[must_use]
    pub fn extra_lod_enabled(&self) -> bool {
        self.flags2.contains(ModelFlags2::EXTRA_LOD_ENABLED)
    }
}

#[derive(Debug, BinRead)]
#[br(little)]
pub(crate) struct WireModelHeader {
    pub radius: f32,
    pub mesh_count: u16,
    pub attribute_count: u16,
    pub submesh_count: u16,
    pub material_count: u16,
    pub bone_count: u16,
    pub bone_table_count: u16,
    pub shape_count: u16,
    pub shape_mesh_count: u16,
    pub shape_value_count: u16,
    pub lod_count: u8,
    pub flags1: u8,
    pub element_id_count: u16,
    pub terrain_shadow_mesh_count: u8,
    pub flags2: u8,
    pub model_clip_out_distance: f32,
    pub shadow_clip_out_distance: f32,
    pub unknown4: u16,
    pub terrain_shadow_submesh_count: u16,
    pub unknown5: u8,
    pub bg_change_material_index: u8,
    pub bg_crest_change_material_index: u8,
    pub unknown6: u8,
    pub unknown7: u16,
    pub unknown8: u16,
    pub unknown9: u16,
    pub _padding: [u8; 6],
}

impl From<WireModelHeader> for ModelHeader {
    fn from(w: WireModelHeader) -> Self {
        ModelHeader {
            radius: w.radius,
            mesh_count: w.mesh_count,
            attribute_count: w.attribute_count,
            submesh_count: w.submesh_count,
            material_count: w.material_count,
            bone_count: w.bone_count,
            bone_table_count: w.bone_table_count,
            shape_count: w.shape_count,
            shape_mesh_count: w.shape_mesh_count,
            shape_value_count: w.shape_value_count,
            lod_count: w.lod_count,
            flags1: ModelFlags1::from_bits(w.flags1),
            element_id_count: w.element_id_count,
            terrain_shadow_mesh_count: w.terrain_shadow_mesh_count,
            flags2: ModelFlags2::from_bits(w.flags2),
            model_clip_out_distance: w.model_clip_out_distance,
            shadow_clip_out_distance: w.shadow_clip_out_distance,
            unknown4: w.unknown4,
            terrain_shadow_submesh_count: w.terrain_shadow_submesh_count,
            unknown5: w.unknown5,
            bg_change_material_index: w.bg_change_material_index,
            bg_crest_change_material_index: w.bg_crest_change_material_index,
            unknown6: w.unknown6,
            unknown7: w.unknown7,
            unknown8: w.unknown8,
            unknown9: w.unknown9,
        }
    }
}

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

    #[test]
    fn model_file_header_size_is_0x44() {
        assert_eq!(MODEL_FILE_HEADER_SIZE, 0x44);
    }

    #[test]
    fn model_header_size_is_0x38() {
        assert_eq!(MODEL_HEADER_SIZE, 0x38);
    }

    #[test]
    fn flag_accessors_round_trip() {
        let f1 =
            ModelFlags1::from_bits(ModelFlags1::DUST_OCCLUSION_ENABLED | ModelFlags1::UNKNOWN1);
        assert!(f1.contains(ModelFlags1::DUST_OCCLUSION_ENABLED));
        assert!(f1.contains(ModelFlags1::UNKNOWN1));
        assert!(!f1.contains(ModelFlags1::SHADOW_DISABLED));
        assert_eq!(
            f1.bits(),
            ModelFlags1::DUST_OCCLUSION_ENABLED | ModelFlags1::UNKNOWN1
        );

        let f2 = ModelFlags2::from_bits(ModelFlags2::EXTRA_LOD_ENABLED);
        assert!(f2.contains(ModelFlags2::EXTRA_LOD_ENABLED));
        assert!(!f2.contains(ModelFlags2::EDGE_GEOMETRY_ENABLED));
    }
}
