use std::io::Cursor;

use binrw::BinRead;

use crate::error::{Error, Result};

use super::header::{PHYB_MAX_RECORDS_PER_SECTION, phyb_string_from_padded};

/// Byte length of the collision count block that precedes all primitives.
pub const COLLISION_COUNT_BLOCK_SIZE: usize = 8;

/// On-disk byte length of a single [`CollisionCapsule`] record.
pub const COLLISION_CAPSULE_SIZE: usize = 124;
/// On-disk byte length of a single [`CollisionEllipsoid`] record.
pub const COLLISION_ELLIPSOID_SIZE: usize = 116;
/// On-disk byte length of a single [`CollisionNormalPlane`] record.
pub const COLLISION_NORMAL_PLANE_SIZE: usize = 92;
/// On-disk byte length of a single [`CollisionThreePointPlane`] record.
pub const COLLISION_THREE_POINT_PLANE_SIZE: usize = 168;
/// On-disk byte length of a single [`CollisionSphere`] record.
pub const COLLISION_SPHERE_SIZE: usize = 80;

/// A capsule collision primitive attached to two bones.
#[derive(Debug, Clone, PartialEq)]
pub struct CollisionCapsule {
    /// Primitive name.
    pub name: String,
    /// Name of the bone at the capsule start.
    pub start_bone_name: String,
    /// Name of the bone at the capsule end.
    pub end_bone_name: String,
    /// Local offset from the start bone in model space.
    pub start_offset: [f32; 3],
    /// Local offset from the end bone in model space.
    pub end_offset: [f32; 3],
    /// Capsule radius.
    pub radius: f32,
}

/// An ellipsoid collision primitive attached to a single bone.
#[derive(Debug, Clone, PartialEq)]
pub struct CollisionEllipsoid {
    /// Primitive name.
    pub name: String,
    /// Name of the attached bone.
    pub bone_name: String,
    /// Four local offsets that define the ellipsoid shape.
    pub offset1: [f32; 3],
    /// Four local offsets that define the ellipsoid shape.
    pub offset2: [f32; 3],
    /// Four local offsets that define the ellipsoid shape.
    pub offset3: [f32; 3],
    /// Four local offsets that define the ellipsoid shape.
    pub offset4: [f32; 3],
    /// Ellipsoid radius.
    pub radius: f32,
}

/// A plane collision primitive defined by a normal vector.
#[derive(Debug, Clone, PartialEq)]
pub struct CollisionNormalPlane {
    /// Primitive name.
    pub name: String,
    /// Name of the attached bone.
    pub bone_name: String,
    /// Plane origin in bone-local space.
    pub bone_offset: [f32; 3],
    /// Plane normal vector.
    pub normal: [f32; 3],
    /// Half-thickness of the collision slab.
    pub thickness: f32,
}

/// A plane collision primitive defined by three implicit points.
///
/// The semantic meaning of the unknown fields is not fully documented.
#[derive(Debug, Clone, PartialEq)]
pub struct CollisionThreePointPlane {
    /// Primitive name.
    pub name: String,
    /// Name of the attached bone.
    pub bone_name: String,
    pub unknown1: [f32; 4],
    pub unknown2: [f32; 4],
    pub unknown3: [f32; 4],
    pub unknown4: [f32; 4],
    /// Plane origin in bone-local space.
    pub bone_offset: [f32; 3],
    pub unknown5: [f32; 3],
    pub unknown6: [f32; 3],
    /// Half-thickness of the collision slab.
    pub thickness: f32,
}

/// A sphere collision primitive attached to a bone.
#[derive(Debug, Clone, PartialEq)]
pub struct CollisionSphere {
    /// Primitive name.
    pub name: String,
    /// Name of the attached bone.
    pub bone_name: String,
    /// Sphere center in bone-local space.
    pub bone_offset: [f32; 3],
    /// Sphere radius.
    pub radius: f32,
}

/// All collision primitives from a single `.phyb` collision section.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CollisionData {
    pub capsules: Vec<CollisionCapsule>,
    pub ellipsoids: Vec<CollisionEllipsoid>,
    pub normal_planes: Vec<CollisionNormalPlane>,
    pub three_point_planes: Vec<CollisionThreePointPlane>,
    pub spheres: Vec<CollisionSphere>,
}

impl CollisionData {
    /// Returns the sum of all primitive counts across all shape types.
    #[must_use]
    pub fn total_primitive_count(&self) -> usize {
        self.capsules.len()
            + self.ellipsoids.len()
            + self.normal_planes.len()
            + self.three_point_planes.len()
            + self.spheres.len()
    }

    pub(crate) fn parse(bytes: &[u8], start: usize) -> Result<Self> {
        let file_size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);

        let count_block_end =
            start
                .checked_add(COLLISION_COUNT_BLOCK_SIZE)
                .ok_or(Error::InvalidPhybOffset {
                    field: "collision.count_block",
                    offset: u64::MAX,
                    file_size,
                })?;
        if count_block_end > bytes.len() {
            return Err(Error::InvalidPhybLength {
                found: bytes.len(),
                minimum: count_block_end,
            });
        }

        let mut cursor = Cursor::new(&bytes[start..count_block_end]);
        let counts = WireCollisionCounts::read(&mut cursor)
            .map_err(|e| Error::decode("phyb.collision.counts", &e))?;

        check_count("num_capsules", u32::from(counts.num_capsules))?;
        check_count("num_ellipsoids", u32::from(counts.num_ellipsoids))?;
        check_count("num_normal_planes", u32::from(counts.num_normal_planes))?;
        check_count(
            "num_three_point_planes",
            u32::from(counts.num_three_point_planes),
        )?;
        check_count("num_spheres", u32::from(counts.num_spheres))?;

        let mut cursor_pos = count_block_end;

        let capsules = read_records::<CollisionCapsule>(
            bytes,
            &mut cursor_pos,
            usize::from(counts.num_capsules),
            COLLISION_CAPSULE_SIZE,
            "phyb.collision.capsule",
        )?;
        let ellipsoids = read_records::<CollisionEllipsoid>(
            bytes,
            &mut cursor_pos,
            usize::from(counts.num_ellipsoids),
            COLLISION_ELLIPSOID_SIZE,
            "phyb.collision.ellipsoid",
        )?;
        let normal_planes = read_records::<CollisionNormalPlane>(
            bytes,
            &mut cursor_pos,
            usize::from(counts.num_normal_planes),
            COLLISION_NORMAL_PLANE_SIZE,
            "phyb.collision.normal_plane",
        )?;
        let three_point_planes = read_records::<CollisionThreePointPlane>(
            bytes,
            &mut cursor_pos,
            usize::from(counts.num_three_point_planes),
            COLLISION_THREE_POINT_PLANE_SIZE,
            "phyb.collision.three_point_plane",
        )?;
        let spheres = read_records::<CollisionSphere>(
            bytes,
            &mut cursor_pos,
            usize::from(counts.num_spheres),
            COLLISION_SPHERE_SIZE,
            "phyb.collision.sphere",
        )?;

        Ok(Self {
            capsules,
            ellipsoids,
            normal_planes,
            three_point_planes,
            spheres,
        })
    }
}

trait FromWireRow: Sized {
    type Wire: for<'a> BinRead<Args<'a> = ()> + binrw::meta::ReadEndian;

    fn from_wire(wire: Self::Wire, record_offset: u64) -> Result<Self>;
}

fn read_records<T: FromWireRow>(
    bytes: &[u8],
    cursor_pos: &mut usize,
    count: usize,
    record_size: usize,
    context: &'static str,
) -> Result<Vec<T>> {
    if count == 0 {
        return Ok(Vec::new());
    }
    let total = record_size
        .checked_mul(count)
        .ok_or(Error::InvalidPhybOffset {
            field: context,
            offset: u64::MAX,
            file_size: u64::try_from(bytes.len()).unwrap_or(u64::MAX),
        })?;
    let end = cursor_pos
        .checked_add(total)
        .ok_or(Error::InvalidPhybOffset {
            field: context,
            offset: u64::MAX,
            file_size: u64::try_from(bytes.len()).unwrap_or(u64::MAX),
        })?;
    if end > bytes.len() {
        return Err(Error::InvalidPhybLength {
            found: bytes.len(),
            minimum: end,
        });
    }

    let array_start = *cursor_pos;
    let mut out = Vec::with_capacity(count);
    let mut cursor = Cursor::new(&bytes[array_start..end]);
    for i in 0..count {
        let wire = T::Wire::read(&mut cursor).map_err(|e| Error::decode(context, &e))?;
        let record_offset = u64::try_from(array_start + i * record_size).unwrap_or(u64::MAX);
        out.push(T::from_wire(wire, record_offset)?);
    }
    *cursor_pos = end;
    Ok(out)
}

fn check_count(kind: &'static str, count: u32) -> Result<()> {
    if count > PHYB_MAX_RECORDS_PER_SECTION {
        Err(Error::InvalidPhybCount {
            kind,
            count,
            max: PHYB_MAX_RECORDS_PER_SECTION,
        })
    } else {
        Ok(())
    }
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireCollisionCounts {
    num_capsules: u8,
    num_ellipsoids: u8,
    num_normal_planes: u8,
    num_three_point_planes: u8,
    num_spheres: u8,
    #[allow(dead_code)]
    padding: [u8; 3],
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireCollisionCapsule {
    name: [u8; 32],
    start_bone_name: [u8; 32],
    end_bone_name: [u8; 32],
    start_offset: [f32; 3],
    end_offset: [f32; 3],
    radius: f32,
}

impl FromWireRow for CollisionCapsule {
    type Wire = WireCollisionCapsule;
    fn from_wire(wire: Self::Wire, record_offset: u64) -> Result<Self> {
        Ok(Self {
            name: phyb_string_from_padded(&wire.name, "capsule.name", record_offset)?,
            start_bone_name: phyb_string_from_padded(
                &wire.start_bone_name,
                "capsule.start_bone_name",
                record_offset + 32,
            )?,
            end_bone_name: phyb_string_from_padded(
                &wire.end_bone_name,
                "capsule.end_bone_name",
                record_offset + 64,
            )?,
            start_offset: wire.start_offset,
            end_offset: wire.end_offset,
            radius: wire.radius,
        })
    }
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireCollisionEllipsoid {
    name: [u8; 32],
    bone_name: [u8; 32],
    offset1: [f32; 3],
    offset2: [f32; 3],
    offset3: [f32; 3],
    offset4: [f32; 3],
    radius: f32,
}

impl FromWireRow for CollisionEllipsoid {
    type Wire = WireCollisionEllipsoid;
    fn from_wire(wire: Self::Wire, record_offset: u64) -> Result<Self> {
        Ok(Self {
            name: phyb_string_from_padded(&wire.name, "ellipsoid.name", record_offset)?,
            bone_name: phyb_string_from_padded(
                &wire.bone_name,
                "ellipsoid.bone_name",
                record_offset + 32,
            )?,
            offset1: wire.offset1,
            offset2: wire.offset2,
            offset3: wire.offset3,
            offset4: wire.offset4,
            radius: wire.radius,
        })
    }
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireCollisionNormalPlane {
    name: [u8; 32],
    bone_name: [u8; 32],
    bone_offset: [f32; 3],
    normal: [f32; 3],
    thickness: f32,
}

impl FromWireRow for CollisionNormalPlane {
    type Wire = WireCollisionNormalPlane;
    fn from_wire(wire: Self::Wire, record_offset: u64) -> Result<Self> {
        Ok(Self {
            name: phyb_string_from_padded(&wire.name, "normal_plane.name", record_offset)?,
            bone_name: phyb_string_from_padded(
                &wire.bone_name,
                "normal_plane.bone_name",
                record_offset + 32,
            )?,
            bone_offset: wire.bone_offset,
            normal: wire.normal,
            thickness: wire.thickness,
        })
    }
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireCollisionThreePointPlane {
    name: [u8; 32],
    bone_name: [u8; 32],
    unknown1: [f32; 4],
    unknown2: [f32; 4],
    unknown3: [f32; 4],
    unknown4: [f32; 4],
    bone_offset: [f32; 3],
    unknown5: [f32; 3],
    unknown6: [f32; 3],
    thickness: f32,
}

impl FromWireRow for CollisionThreePointPlane {
    type Wire = WireCollisionThreePointPlane;
    fn from_wire(wire: Self::Wire, record_offset: u64) -> Result<Self> {
        Ok(Self {
            name: phyb_string_from_padded(&wire.name, "three_point_plane.name", record_offset)?,
            bone_name: phyb_string_from_padded(
                &wire.bone_name,
                "three_point_plane.bone_name",
                record_offset + 32,
            )?,
            unknown1: wire.unknown1,
            unknown2: wire.unknown2,
            unknown3: wire.unknown3,
            unknown4: wire.unknown4,
            bone_offset: wire.bone_offset,
            unknown5: wire.unknown5,
            unknown6: wire.unknown6,
            thickness: wire.thickness,
        })
    }
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireCollisionSphere {
    name: [u8; 32],
    bone_name: [u8; 32],
    bone_offset: [f32; 3],
    thickness: f32,
}

impl FromWireRow for CollisionSphere {
    type Wire = WireCollisionSphere;
    fn from_wire(wire: Self::Wire, record_offset: u64) -> Result<Self> {
        Ok(Self {
            name: phyb_string_from_padded(&wire.name, "sphere.name", record_offset)?,
            bone_name: phyb_string_from_padded(
                &wire.bone_name,
                "sphere.bone_name",
                record_offset + 32,
            )?,
            bone_offset: wire.bone_offset,
            radius: wire.thickness,
        })
    }
}

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

    fn name_buf(s: &str) -> [u8; 32] {
        let mut out = [0u8; 32];
        let bytes = s.as_bytes();
        let n = bytes.len().min(32);
        out[..n].copy_from_slice(&bytes[..n]);
        out
    }

    fn build_capsule_bytes(name: &str, start: &str, end: &str) -> Vec<u8> {
        let mut buf = Vec::with_capacity(COLLISION_CAPSULE_SIZE);
        buf.extend_from_slice(&name_buf(name));
        buf.extend_from_slice(&name_buf(start));
        buf.extend_from_slice(&name_buf(end));
        for v in [0.1f32, 0.2, 0.3, -0.4, -0.5, -0.6, 1.5] {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        assert_eq!(buf.len(), COLLISION_CAPSULE_SIZE);
        buf
    }

    fn build_sphere_bytes(name: &str, bone: &str) -> Vec<u8> {
        let mut buf = Vec::with_capacity(COLLISION_SPHERE_SIZE);
        buf.extend_from_slice(&name_buf(name));
        buf.extend_from_slice(&name_buf(bone));
        for v in [0.7f32, 0.8, 0.9, 2.0] {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        assert_eq!(buf.len(), COLLISION_SPHERE_SIZE);
        buf
    }

    fn build_empty_count_block() -> [u8; COLLISION_COUNT_BLOCK_SIZE] {
        let mut buf = [0u8; COLLISION_COUNT_BLOCK_SIZE];
        buf[5] = 0xCC;
        buf[6] = 0xCC;
        buf[7] = 0xCC;
        buf
    }

    #[test]
    fn parses_empty_section() {
        let bytes = build_empty_count_block();
        let coll = CollisionData::parse(&bytes, 0).expect("parse");
        assert!(coll.capsules.is_empty());
        assert!(coll.ellipsoids.is_empty());
        assert!(coll.normal_planes.is_empty());
        assert!(coll.three_point_planes.is_empty());
        assert!(coll.spheres.is_empty());
        assert_eq!(coll.total_primitive_count(), 0);
    }

    #[test]
    fn parses_one_capsule_one_sphere() {
        let mut bytes = Vec::new();
        let mut counts = [0u8; COLLISION_COUNT_BLOCK_SIZE];
        counts[0] = 1;
        counts[4] = 1;
        bytes.extend_from_slice(&counts);
        bytes.extend_from_slice(&build_capsule_bytes("cap0", "j_kao", "j_kubi"));
        bytes.extend_from_slice(&build_sphere_bytes("sph0", "j_kao"));

        let coll = CollisionData::parse(&bytes, 0).expect("parse");
        assert_eq!(coll.capsules.len(), 1);
        assert_eq!(coll.spheres.len(), 1);

        let cap = &coll.capsules[0];
        assert_eq!(cap.name, "cap0");
        assert_eq!(cap.start_bone_name, "j_kao");
        assert_eq!(cap.end_bone_name, "j_kubi");
        assert!((cap.radius - 1.5).abs() < f32::EPSILON);

        let sph = &coll.spheres[0];
        assert_eq!(sph.name, "sph0");
        assert_eq!(sph.bone_name, "j_kao");
        assert!((sph.radius - 2.0).abs() < f32::EPSILON);

        assert_eq!(coll.total_primitive_count(), 2);
    }

    #[test]
    fn rejects_truncated_count_block() {
        let bytes = vec![0u8; COLLISION_COUNT_BLOCK_SIZE - 1];
        let err = CollisionData::parse(&bytes, 0).unwrap_err();
        assert!(matches!(err, Error::InvalidPhybLength { .. }));
    }

    #[test]
    fn rejects_truncated_record_array() {
        let mut bytes = Vec::new();
        let mut counts = [0u8; COLLISION_COUNT_BLOCK_SIZE];
        counts[0] = 1;
        bytes.extend_from_slice(&counts);
        let err = CollisionData::parse(&bytes, 0).unwrap_err();
        match err {
            Error::InvalidPhybLength { found, minimum } => {
                assert_eq!(found, COLLISION_COUNT_BLOCK_SIZE);
                assert_eq!(minimum, COLLISION_COUNT_BLOCK_SIZE + COLLISION_CAPSULE_SIZE);
            }
            other => panic!("expected InvalidPhybLength, got {other:?}"),
        }
    }

    #[test]
    fn rejects_collision_offset_past_eof_via_short_input() {
        let bytes = vec![0u8; 4];
        let err = CollisionData::parse(&bytes, 10).unwrap_err();
        assert!(matches!(err, Error::InvalidPhybLength { .. }));
    }

    #[test]
    fn name_field_rejects_invalid_utf8() {
        let mut bytes = Vec::new();
        let mut counts = [0u8; COLLISION_COUNT_BLOCK_SIZE];
        counts[4] = 1;
        bytes.extend_from_slice(&counts);

        let mut sphere = vec![0u8; COLLISION_SPHERE_SIZE];
        sphere[0] = 0xFF;
        sphere[1] = b'x';
        bytes.extend_from_slice(&sphere);

        let err = CollisionData::parse(&bytes, 0).unwrap_err();
        match err {
            Error::InvalidUtf8InPhybString { field, .. } => {
                assert_eq!(field, "sphere.name");
            }
            other => panic!("expected InvalidUtf8InPhybString, got {other:?}"),
        }
    }

    fn build_three_point_plane_bytes(name: &str, bone: &str) -> Vec<u8> {
        let mut buf = Vec::with_capacity(COLLISION_THREE_POINT_PLANE_SIZE);
        buf.extend_from_slice(&name_buf(name));
        buf.extend_from_slice(&name_buf(bone));
        for v in [
            1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0,
            16.0,
        ] {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        for v in [0.5f32, -0.25, 1.5] {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        for v in [17.0f32, 18.0, 19.0] {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        for v in [20.0f32, 21.0, 22.0] {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        buf.extend_from_slice(&0.75f32.to_le_bytes());
        assert_eq!(buf.len(), COLLISION_THREE_POINT_PLANE_SIZE);
        buf
    }

    fn build_ellipsoid_bytes(name: &str, bone: &str) -> Vec<u8> {
        let mut buf = Vec::with_capacity(COLLISION_ELLIPSOID_SIZE);
        buf.extend_from_slice(&name_buf(name));
        buf.extend_from_slice(&name_buf(bone));
        for v in [
            1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
        ] {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        buf.extend_from_slice(&1.75f32.to_le_bytes());
        assert_eq!(buf.len(), COLLISION_ELLIPSOID_SIZE);
        buf
    }

    fn build_normal_plane_bytes(name: &str, bone: &str) -> Vec<u8> {
        let mut buf = Vec::with_capacity(COLLISION_NORMAL_PLANE_SIZE);
        buf.extend_from_slice(&name_buf(name));
        buf.extend_from_slice(&name_buf(bone));
        for v in [0.1f32, 0.2, 0.3] {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        for v in [0.0f32, 1.0, 0.0] {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        buf.extend_from_slice(&0.05f32.to_le_bytes());
        assert_eq!(buf.len(), COLLISION_NORMAL_PLANE_SIZE);
        buf
    }

    #[test]
    fn capsule_round_trip_offset_vectors() {
        let mut bytes = Vec::new();
        let mut counts = [0u8; COLLISION_COUNT_BLOCK_SIZE];
        counts[0] = 1;
        bytes.extend_from_slice(&counts);
        bytes.extend_from_slice(&build_capsule_bytes("cap_rt", "j_s", "j_e"));

        let coll = CollisionData::parse(&bytes, 0).expect("parse");
        let cap = &coll.capsules[0];
        assert_eq!(cap.name, "cap_rt");
        assert_eq!(cap.start_bone_name, "j_s");
        assert_eq!(cap.end_bone_name, "j_e");
        let bits3 = |arr: [f32; 3]| arr.map(f32::to_bits);
        assert_eq!(bits3(cap.start_offset), bits3([0.1, 0.2, 0.3]));
        assert_eq!(bits3(cap.end_offset), bits3([-0.4, -0.5, -0.6]));
        assert_eq!(cap.radius.to_bits(), 1.5f32.to_bits());
    }

    #[test]
    fn sphere_round_trip_offset_vector() {
        let mut bytes = Vec::new();
        let mut counts = [0u8; COLLISION_COUNT_BLOCK_SIZE];
        counts[4] = 1;
        bytes.extend_from_slice(&counts);
        bytes.extend_from_slice(&build_sphere_bytes("sph_rt", "j_b"));

        let coll = CollisionData::parse(&bytes, 0).expect("parse");
        let sph = &coll.spheres[0];
        assert_eq!(sph.name, "sph_rt");
        assert_eq!(sph.bone_name, "j_b");
        let bits3 = |arr: [f32; 3]| arr.map(f32::to_bits);
        assert_eq!(bits3(sph.bone_offset), bits3([0.7, 0.8, 0.9]));
        assert_eq!(sph.radius.to_bits(), 2.0f32.to_bits());
    }

    #[test]
    fn ellipsoid_round_trip() {
        let mut bytes = Vec::new();
        let mut counts = [0u8; COLLISION_COUNT_BLOCK_SIZE];
        counts[1] = 1;
        bytes.extend_from_slice(&counts);
        bytes.extend_from_slice(&build_ellipsoid_bytes("ell_rt", "j_spine"));

        let coll = CollisionData::parse(&bytes, 0).expect("parse");
        assert_eq!(coll.ellipsoids.len(), 1);
        let e = &coll.ellipsoids[0];
        assert_eq!(e.name, "ell_rt");
        assert_eq!(e.bone_name, "j_spine");
        let bits3 = |arr: [f32; 3]| arr.map(f32::to_bits);
        assert_eq!(bits3(e.offset1), bits3([1.0, 2.0, 3.0]));
        assert_eq!(bits3(e.offset2), bits3([4.0, 5.0, 6.0]));
        assert_eq!(bits3(e.offset3), bits3([7.0, 8.0, 9.0]));
        assert_eq!(bits3(e.offset4), bits3([10.0, 11.0, 12.0]));
        assert_eq!(e.radius.to_bits(), 1.75f32.to_bits());
    }

    #[test]
    fn normal_plane_round_trip() {
        let mut bytes = Vec::new();
        let mut counts = [0u8; COLLISION_COUNT_BLOCK_SIZE];
        counts[2] = 1;
        bytes.extend_from_slice(&counts);
        bytes.extend_from_slice(&build_normal_plane_bytes("np_rt", "j_chest"));

        let coll = CollisionData::parse(&bytes, 0).expect("parse");
        assert_eq!(coll.normal_planes.len(), 1);
        let np = &coll.normal_planes[0];
        assert_eq!(np.name, "np_rt");
        assert_eq!(np.bone_name, "j_chest");
        let bits3 = |arr: [f32; 3]| arr.map(f32::to_bits);
        assert_eq!(bits3(np.bone_offset), bits3([0.1, 0.2, 0.3]));
        assert_eq!(bits3(np.normal), bits3([0.0, 1.0, 0.0]));
        assert_eq!(np.thickness.to_bits(), 0.05f32.to_bits());
    }

    #[test]
    fn parses_collision_three_point_plane_round_trip() {
        let mut bytes = Vec::new();
        let mut counts = [0u8; COLLISION_COUNT_BLOCK_SIZE];
        counts[3] = 1;
        bytes.extend_from_slice(&counts);
        bytes.extend_from_slice(&build_three_point_plane_bytes("3pp_test", "j_root"));

        let coll = CollisionData::parse(&bytes, 0).expect("parse");
        assert_eq!(coll.three_point_planes.len(), 1);
        let tpp = &coll.three_point_planes[0];
        assert_eq!(tpp.name, "3pp_test");
        assert_eq!(tpp.bone_name, "j_root");
        let bits4 = |arr: [f32; 4]| arr.map(f32::to_bits);
        let bits3 = |arr: [f32; 3]| arr.map(f32::to_bits);
        assert_eq!(bits4(tpp.unknown1), bits4([1.0, 2.0, 3.0, 4.0]));
        assert_eq!(bits4(tpp.unknown2), bits4([5.0, 6.0, 7.0, 8.0]));
        assert_eq!(bits4(tpp.unknown3), bits4([9.0, 10.0, 11.0, 12.0]));
        assert_eq!(bits4(tpp.unknown4), bits4([13.0, 14.0, 15.0, 16.0]));
        assert_eq!(bits3(tpp.bone_offset), bits3([0.5, -0.25, 1.5]));
        assert_eq!(bits3(tpp.unknown5), bits3([17.0, 18.0, 19.0]));
        assert_eq!(bits3(tpp.unknown6), bits3([20.0, 21.0, 22.0]));
        assert_eq!(tpp.thickness.to_bits(), 0.75f32.to_bits());
    }
}
