// P3G file format parser — reads meshlete's native binary format.
// Converts P3G meshlets to GpuMeshlet for GPU upload via MeshletBuffer.
//
// File layout: header (40B) → segments (16B each) → meshlet descriptors (8-16B each)
// → vertex index buffer → triangle index buffer → optional vertex buffer.
//
// No GPU dependency in this module — pure binary parsing + conversion.
// CODESPEC §4.1: all allocation happens at import time (cold path).

use crate::meshlet::{GpuMeshlet, MESHLET_MAX_TRIANGLES, MESHLET_MAX_VERTICES};

// --- Constants ---

pub const P3G_MAGIC: [u8; 4] = *b"p3dg";
pub const P3G_VERSION_1_0: u16 = 0x1000;
pub const P3G_HEADER_SIZE: usize = 40;
pub const P3G_SEGMENT_SIZE: usize = 16;

const FLAG_32BIT_INDEX: u16 = 0x0001;
const FLAG_TRISTRIPS: u16 = 0x0002;
const FLAG_BVOLS: u16 = 0x0004;
const FLAG_VCONES: u16 = 0x0008;

// --- Error type ---

#[derive(Debug)]
pub enum P3gError {
    TooSmall { expected: usize, got: usize },
    BadMagic([u8; 4]),
    UnsupportedVersion(u16),
    Tristrips,
    UnsupportedVertexFormat(u8),
    FileSizeMismatch { header: u32, actual: usize },
    MeshletBudgetExceeded { index: u16, vertices: u32, triangles: u32 },
    InvalidOffset { context: &'static str },
    NoVertexBuffer,
}

impl std::fmt::Display for P3gError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::TooSmall { expected, got } => {
                write!(f, "p3g_too_small:expected {} bytes, got {}", expected, got)
            }
            Self::BadMagic(m) => write!(f, "p3g_bad_magic:{:?}", m),
            Self::UnsupportedVersion(v) => write!(f, "p3g_version:0x{:04X} unsupported", v),
            Self::Tristrips => write!(f, "p3g_tristrips:triangle strips not supported"),
            Self::UnsupportedVertexFormat(id) => {
                write!(f, "p3g_vertex_format:vfmt_id {} not supported", id)
            }
            Self::FileSizeMismatch { header, actual } => {
                write!(f, "p3g_size_mismatch:header says {} bytes, file is {}", header, actual)
            }
            Self::MeshletBudgetExceeded {
                index,
                vertices,
                triangles,
            } => write!(
                f,
                "p3g_budget:meshlet[{}] has {} verts/{} tris (max {}/{})",
                index, vertices, triangles, MESHLET_MAX_VERTICES, MESHLET_MAX_TRIANGLES
            ),
            Self::InvalidOffset { context } => {
                write!(f, "p3g_offset:invalid offset in {}", context)
            }
            Self::NoVertexBuffer => {
                write!(f, "p3g_no_vbuf:file has no vertex buffer (vbuf_size=0)")
            }
        }
    }
}

impl std::error::Error for P3gError {}

// --- Parsed structures ---

#[derive(Debug, Clone)]
pub struct P3gHeader {
    pub version: u16,
    pub flags: u16,
    pub total_file_size: u32,
    pub num_meshlets: u16,
    pub num_segments: u8,
    pub vfmt_id: u8,
    pub vbuf_offset: u32,
    pub vbuf_size: u32,
    pub bounding_sphere: [f32; 4], // xyz=center, w=radius
}

#[derive(Debug, Clone)]
pub struct P3gSegment {
    pub material_id: u32,
    pub start_meshlet: u16,
    pub num_meshlets: u16,
    pub bvol_center: [f32; 3],
    pub bvol_radius: f32,
}

/// Parsed and dequantized meshlet descriptor.
#[derive(Debug, Clone)]
pub struct P3gMeshletDesc {
    pub vertex_offset: u32,
    pub vertex_count: u32,
    pub triangle_offset: u32,
    pub triangle_count: u32,
    pub bvol_center: [f32; 3],
    pub bvol_radius: f32,
    pub vcone_dir: [f32; 3],
    pub vcone_dot: f32,
    pub has_bvol: bool,
    pub has_vcone: bool,
}

/// Complete parsed P3G file.
#[derive(Debug)]
pub struct P3gFile {
    pub header: P3gHeader,
    pub segments: Vec<P3gSegment>,
    pub meshlets: Vec<P3gMeshletDesc>,
    pub vertex_indices: Vec<u32>,
    pub triangle_indices: Vec<u8>,
    pub vertex_positions: Vec<[f32; 3]>,
}

/// Result of converting P3G to engine-ready format.
pub struct P3gImportResult {
    pub meshlets: Vec<GpuMeshlet>,
    pub vertices: Vec<[f32; 3]>,
    pub triangles: Vec<u8>,
    pub bounding_sphere: [f32; 4],
    pub stats: P3gStats,
}

#[derive(Debug, Clone)]
pub struct P3gStats {
    pub meshlet_count: usize,
    pub vertex_count: usize,
    pub triangle_count: usize,
    pub segment_count: usize,
    pub file_size: usize,
}

// --- Byte reading helpers ---

fn read_u8(data: &[u8], offset: usize) -> Result<u8, P3gError> {
    data.get(offset).copied().ok_or(P3gError::TooSmall {
        expected: offset + 1,
        got: data.len(),
    })
}

fn read_u16_le(data: &[u8], offset: usize) -> Result<u16, P3gError> {
    let end = offset + 2;
    if end > data.len() {
        return Err(P3gError::TooSmall {
            expected: end,
            got: data.len(),
        });
    }
    Ok(u16::from_le_bytes([data[offset], data[offset + 1]]))
}

fn read_i16_le(data: &[u8], offset: usize) -> Result<i16, P3gError> {
    let end = offset + 2;
    if end > data.len() {
        return Err(P3gError::TooSmall {
            expected: end,
            got: data.len(),
        });
    }
    Ok(i16::from_le_bytes([data[offset], data[offset + 1]]))
}

fn read_u32_le(data: &[u8], offset: usize) -> Result<u32, P3gError> {
    let end = offset + 4;
    if end > data.len() {
        return Err(P3gError::TooSmall {
            expected: end,
            got: data.len(),
        });
    }
    Ok(u32::from_le_bytes([
        data[offset],
        data[offset + 1],
        data[offset + 2],
        data[offset + 3],
    ]))
}

fn read_f32_le(data: &[u8], offset: usize) -> Result<f32, P3gError> {
    Ok(f32::from_bits(read_u32_le(data, offset)?))
}

fn read_i8(data: &[u8], offset: usize) -> Result<i8, P3gError> {
    data.get(offset).map(|&b| b as i8).ok_or(P3gError::TooSmall {
        expected: offset + 1,
        got: data.len(),
    })
}

// --- Parser ---

/// Parse a P3G binary file into structured data.
pub fn parse_p3g(data: &[u8]) -> Result<P3gFile, P3gError> {
    if data.len() < P3G_HEADER_SIZE {
        return Err(P3gError::TooSmall {
            expected: P3G_HEADER_SIZE,
            got: data.len(),
        });
    }

    // Header
    let magic = [data[0], data[1], data[2], data[3]];
    if magic != P3G_MAGIC {
        return Err(P3gError::BadMagic(magic));
    }

    let version = read_u16_le(data, 4)?;
    if version != P3G_VERSION_1_0 {
        return Err(P3gError::UnsupportedVersion(version));
    }

    let flags = read_u16_le(data, 6)?;
    if flags & FLAG_TRISTRIPS != 0 {
        return Err(P3gError::Tristrips);
    }

    let total_file_size = read_u32_le(data, 8)?;
    let num_meshlets = read_u16_le(data, 12)?;
    let num_segments = read_u8(data, 14)?;
    let vfmt_id = read_u8(data, 15)?;
    let vbuf_offset = read_u32_le(data, 16)?;
    let vbuf_size = read_u32_le(data, 20)?;

    let bsphere_x = read_f32_le(data, 24)?;
    let bsphere_y = read_f32_le(data, 28)?;
    let bsphere_z = read_f32_le(data, 32)?;
    let bsphere_r = read_f32_le(data, 36)?;

    let header = P3gHeader {
        version,
        flags,
        total_file_size,
        num_meshlets,
        num_segments,
        vfmt_id,
        vbuf_offset,
        vbuf_size,
        bounding_sphere: [bsphere_x, bsphere_y, bsphere_z, bsphere_r],
    };

    let has_32bit_idx = flags & FLAG_32BIT_INDEX != 0;
    let has_bvols = flags & FLAG_BVOLS != 0;
    let has_vcones = flags & FLAG_VCONES != 0;

    // Segment table
    let seg_start = P3G_HEADER_SIZE;
    let seg_end = seg_start + (num_segments as usize) * P3G_SEGMENT_SIZE;
    if seg_end > data.len() {
        return Err(P3gError::TooSmall {
            expected: seg_end,
            got: data.len(),
        });
    }

    let mesh_bvol_pos = [bsphere_x, bsphere_y, bsphere_z];
    let mesh_bvol_rad = bsphere_r;

    let mut segments = Vec::with_capacity(num_segments as usize);
    for i in 0..num_segments as usize {
        let o = seg_start + i * P3G_SEGMENT_SIZE;
        let material_id = read_u32_le(data, o)?;
        let start_meshlet = read_u16_le(data, o + 4)?;
        let seg_num_meshlets = read_u16_le(data, o + 6)?;

        // Dequantize segment bounding volume relative to mesh bounding sphere
        let qx = read_i16_le(data, o + 8)? as f32;
        let qy = read_i16_le(data, o + 10)? as f32;
        let qz = read_i16_le(data, o + 12)? as f32;
        let qr = read_u16_le(data, o + 14)? as f32;

        let bvol_center = [
            mesh_bvol_pos[0] + qx * mesh_bvol_rad / 32767.0,
            mesh_bvol_pos[1] + qy * mesh_bvol_rad / 32767.0,
            mesh_bvol_pos[2] + qz * mesh_bvol_rad / 32767.0,
        ];
        let bvol_radius = qr * mesh_bvol_rad / 65535.0;

        segments.push(P3gSegment {
            material_id,
            start_meshlet,
            num_meshlets: seg_num_meshlets,
            bvol_center,
            bvol_radius,
        });
    }

    // Meshlet descriptors
    let mlet_desc_size = 8 + if has_bvols { 4 } else { 0 } + if has_vcones { 4 } else { 0 };
    let mlet_start = seg_end;
    let mlet_end = mlet_start + (num_meshlets as usize) * mlet_desc_size;
    if mlet_end > data.len() {
        return Err(P3gError::TooSmall {
            expected: mlet_end,
            got: data.len(),
        });
    }

    let mut meshlet_descs = Vec::with_capacity(num_meshlets as usize);
    for i in 0..num_meshlets as usize {
        let o = mlet_start + i * mlet_desc_size;
        let vidx_packed = read_u32_le(data, o)?;
        let tidx_packed = read_u32_le(data, o + 4)?;

        let vertex_offset = vidx_packed & 0x00FF_FFFF;
        let vertex_count = (vidx_packed >> 24) & 0xFF;
        let triangle_offset = tidx_packed & 0x00FF_FFFF;
        let triangle_count = (tidx_packed >> 24) & 0xFF;

        if vertex_count > MESHLET_MAX_VERTICES {
            return Err(P3gError::MeshletBudgetExceeded {
                index: i as u16,
                vertices: vertex_count,
                triangles: triangle_count,
            });
        }

        // Find which segment this meshlet belongs to for bvol dequantization
        let seg = segments.iter().find(|s| {
            let idx = i as u16;
            idx >= s.start_meshlet && idx < s.start_meshlet + s.num_meshlets
        });

        let mut bvol_center = [0.0f32; 3];
        let mut bvol_radius = 0.0f32;
        let mut has_mlet_bvol = false;

        let mut extra_off = o + 8;

        if has_bvols {
            let qx = read_i8(data, extra_off)? as f32;
            let qy = read_i8(data, extra_off + 1)? as f32;
            let qz = read_i8(data, extra_off + 2)? as f32;
            let qr = read_u8(data, extra_off + 3)? as f32;
            extra_off += 4;

            if let Some(seg) = seg {
                bvol_center = [
                    seg.bvol_center[0] + qx * seg.bvol_radius / 127.0,
                    seg.bvol_center[1] + qy * seg.bvol_radius / 127.0,
                    seg.bvol_center[2] + qz * seg.bvol_radius / 127.0,
                ];
                bvol_radius = qr * seg.bvol_radius / 255.0;
            }
            has_mlet_bvol = true;
        }

        let mut vcone_dir = [0.0f32; 3];
        let mut vcone_dot = -1.0f32;
        let mut has_mlet_vcone = false;

        if has_vcones {
            let dx = read_i8(data, extra_off)? as f32;
            let dy = read_i8(data, extra_off + 1)? as f32;
            let dz = read_i8(data, extra_off + 2)? as f32;
            let dot = read_i8(data, extra_off + 3)? as f32;

            vcone_dir = [dx / 127.0, dy / 127.0, dz / 127.0];
            vcone_dot = (dot + 0.5) / 127.5;
            has_mlet_vcone = true;
        }

        meshlet_descs.push(P3gMeshletDesc {
            vertex_offset,
            vertex_count,
            triangle_offset,
            triangle_count,
            bvol_center,
            bvol_radius,
            vcone_dir,
            vcone_dot,
            has_bvol: has_mlet_bvol,
            has_vcone: has_mlet_vcone,
        });
    }

    // Vertex index buffer (after meshlet descriptors, aligned to 4 bytes)
    let vidx_start = align4(mlet_end);
    let vidx_elem_size = if has_32bit_idx { 4 } else { 2 };

    // Count total vertex indices across all meshlets
    let total_vidx_elements: usize = meshlet_descs
        .iter()
        .map(|m| (m.vertex_offset + m.vertex_count) as usize)
        .max()
        .unwrap_or(0);

    let vidx_byte_len = total_vidx_elements * vidx_elem_size;
    let vidx_end = vidx_start + vidx_byte_len;

    if vidx_end > data.len() {
        return Err(P3gError::TooSmall {
            expected: vidx_end,
            got: data.len(),
        });
    }

    let mut vertex_indices = Vec::with_capacity(total_vidx_elements);
    for i in 0..total_vidx_elements {
        let off = vidx_start + i * vidx_elem_size;
        let idx = if has_32bit_idx {
            read_u32_le(data, off)?
        } else {
            read_u16_le(data, off)? as u32
        };
        vertex_indices.push(idx);
    }

    // Triangle index buffer (after vertex indices, aligned to 4 bytes)
    let tidx_start = align4(vidx_end);
    let total_tidx_elements: usize = meshlet_descs
        .iter()
        .map(|m| (m.triangle_offset + m.triangle_count * 3) as usize)
        .max()
        .unwrap_or(0);

    let tidx_end = tidx_start + total_tidx_elements;
    if tidx_end > data.len() {
        return Err(P3gError::TooSmall {
            expected: tidx_end,
            got: data.len(),
        });
    }

    let triangle_indices = data[tidx_start..tidx_end].to_vec();

    // Vertex buffer (position extraction based on vertex format)
    let mut vertex_positions = Vec::new();

    if vbuf_size > 0 && vbuf_offset > 0 {
        let vbuf_start = vbuf_offset as usize;
        let vbuf_end = vbuf_start + vbuf_size as usize;
        if vbuf_end > data.len() {
            return Err(P3gError::TooSmall {
                expected: vbuf_end,
                got: data.len(),
            });
        }

        let vbuf = &data[vbuf_start..vbuf_end];
        vertex_positions = extract_positions(vbuf, vfmt_id, &header)?;
    }

    Ok(P3gFile {
        header,
        segments,
        meshlets: meshlet_descs,
        vertex_indices,
        triangle_indices,
        vertex_positions,
    })
}

/// Convert a parsed P3G file to engine-ready GpuMeshlet data.
///
/// `layer_index` assigns the topology layer for all meshlets.
/// `object_id` assigns the entity object ID for picking.
pub fn convert_to_gpu_meshlets(file: &P3gFile, layer_index: u32, object_id: u32) -> P3gImportResult {
    // Build the flat vertex buffer: for each meshlet, resolve vertex indices
    // to actual positions and pack contiguously.
    let mut flat_vertices: Vec<[f32; 3]> = Vec::new();
    let mut flat_triangles: Vec<u8> = Vec::new();
    let mut gpu_meshlets: Vec<GpuMeshlet> = Vec::with_capacity(file.meshlets.len());

    for desc in &file.meshlets {
        let gpu_vtx_offset = flat_vertices.len() as u32;
        let gpu_tri_offset = flat_triangles.len() as u32;

        // Resolve vertex indices to positions
        for v in 0..desc.vertex_count {
            let vidx = desc.vertex_offset + v;
            if (vidx as usize) < file.vertex_indices.len() {
                let global_idx = file.vertex_indices[vidx as usize] as usize;
                if global_idx < file.vertex_positions.len() {
                    flat_vertices.push(file.vertex_positions[global_idx]);
                } else {
                    flat_vertices.push([0.0; 3]);
                }
            } else {
                flat_vertices.push([0.0; 3]);
            }
        }

        // Copy triangle indices (already local to meshlet vertex set)
        let tri_start = desc.triangle_offset as usize;
        let tri_count = (desc.triangle_count * 3) as usize;
        let tri_end = tri_start + tri_count;
        if tri_end <= file.triangle_indices.len() {
            flat_triangles.extend_from_slice(&file.triangle_indices[tri_start..tri_end]);
        } else {
            flat_triangles.extend(std::iter::repeat_n(0u8, tri_count));
        }

        // Build bounding sphere: use dequantized if available, else from header
        let bounding_sphere = if desc.has_bvol {
            [
                desc.bvol_center[0],
                desc.bvol_center[1],
                desc.bvol_center[2],
                desc.bvol_radius,
            ]
        } else {
            file.header.bounding_sphere
        };

        gpu_meshlets.push(GpuMeshlet {
            vertex_offset: gpu_vtx_offset,
            vertex_count: desc.vertex_count,
            triangle_offset: gpu_tri_offset,
            triangle_count: desc.triangle_count,
            layer_index,
            object_id,
            bounding_sphere,
            lod_level: 0,
            _pad: 0,
        });
    }

    let stats = P3gStats {
        meshlet_count: gpu_meshlets.len(),
        vertex_count: flat_vertices.len(),
        triangle_count: flat_triangles.len() / 3,
        segment_count: file.segments.len(),
        file_size: file.header.total_file_size as usize,
    };

    P3gImportResult {
        meshlets: gpu_meshlets,
        vertices: flat_vertices,
        triangles: flat_triangles,
        bounding_sphere: file.header.bounding_sphere,
        stats,
    }
}

/// Load and convert a P3G file in one call (convenience).
pub fn load_p3g(data: &[u8], layer_index: u32, object_id: u32) -> Result<P3gImportResult, P3gError> {
    let file = parse_p3g(data)?;
    Ok(convert_to_gpu_meshlets(&file, layer_index, object_id))
}

// --- Vertex format extraction ---

/// Extract float32 positions from a vertex buffer based on vfmt_id.
fn extract_positions(vbuf: &[u8], vfmt_id: u8, header: &P3gHeader) -> Result<Vec<[f32; 3]>, P3gError> {
    let (stride, pos_offset) = vertex_format_layout(vfmt_id)?;
    let num_vertices = vbuf.len() / stride;
    let mut positions = Vec::with_capacity(num_vertices);

    let is_quantized = matches!(vfmt_id, 6..=10);

    for i in 0..num_vertices {
        let base = i * stride + pos_offset;

        if is_quantized {
            // Quantized: int16[3], dequantize relative to bounding sphere
            if base + 6 > vbuf.len() {
                break;
            }
            let qx = i16::from_le_bytes([vbuf[base], vbuf[base + 1]]) as f32;
            let qy = i16::from_le_bytes([vbuf[base + 2], vbuf[base + 3]]) as f32;
            let qz = i16::from_le_bytes([vbuf[base + 4], vbuf[base + 5]]) as f32;

            let rad = header.bounding_sphere[3];
            let cx = header.bounding_sphere[0];
            let cy = header.bounding_sphere[1];
            let cz = header.bounding_sphere[2];

            positions.push([
                cx + qx * rad / 32767.0,
                cy + qy * rad / 32767.0,
                cz + qz * rad / 32767.0,
            ]);
        } else {
            // Float32 positions
            if base + 12 > vbuf.len() {
                break;
            }
            let x = f32::from_le_bytes([vbuf[base], vbuf[base + 1], vbuf[base + 2], vbuf[base + 3]]);
            let y = f32::from_le_bytes([vbuf[base + 4], vbuf[base + 5], vbuf[base + 6], vbuf[base + 7]]);
            let z = f32::from_le_bytes([vbuf[base + 8], vbuf[base + 9], vbuf[base + 10], vbuf[base + 11]]);
            positions.push([x, y, z]);
        }
    }

    Ok(positions)
}

/// Returns (stride_bytes, position_offset_bytes) for each supported vertex format.
fn vertex_format_layout(vfmt_id: u8) -> Result<(usize, usize), P3gError> {
    match vfmt_id {
        1 => Ok((12, 0)),  // p: float32 xyz
        2 => Ok((24, 0)),  // pn: float32 xyz + float32 normal
        3 => Ok((16, 0)),  // pq: float32 xyz + u32 quaternion
        4 => Ok((32, 0)),  // pnu: float32 xyz + normal + uv
        5 => Ok((24, 0)),  // pqu: float32 xyz + quaternion + uv
        6 => Ok((6, 0)),   // p48: int16 xyz (quantized)
        7 => Ok((10, 0)),  // p48n32: int16 xyz + u32 normal
        8 => Ok((10, 0)),  // p48tbn32: int16 xyz + u32 TBN
        9 => Ok((14, 0)),  // p48n32uv32: int16 xyz + u32 normal + u32 uv
        10 => Ok((14, 0)), // p48tbn32uv32: int16 xyz + u32 TBN + u32 uv
        _ => Err(P3gError::UnsupportedVertexFormat(vfmt_id)),
    }
}

fn align4(v: usize) -> usize {
    (v + 3) & !3
}

// --- Tests ---

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

    fn make_minimal_p3g(num_meshlets: u16, num_segments: u8, flags: u16) -> Vec<u8> {
        let mut data = Vec::new();

        // Header (40 bytes)
        data.extend_from_slice(&P3G_MAGIC);
        data.extend_from_slice(&P3G_VERSION_1_0.to_le_bytes());
        data.extend_from_slice(&flags.to_le_bytes());
        // total_file_size — placeholder, updated below
        data.extend_from_slice(&0u32.to_le_bytes());
        data.extend_from_slice(&num_meshlets.to_le_bytes());
        data.push(num_segments);
        data.push(1); // vfmt_id = 1 (float32 xyz)
                      // vbuf_offset, vbuf_size — placeholders
        data.extend_from_slice(&0u32.to_le_bytes());
        data.extend_from_slice(&0u32.to_le_bytes());
        // bounding sphere
        data.extend_from_slice(&0.0f32.to_le_bytes()); // x
        data.extend_from_slice(&0.0f32.to_le_bytes()); // y
        data.extend_from_slice(&0.0f32.to_le_bytes()); // z
        data.extend_from_slice(&1.5f32.to_le_bytes()); // radius

        assert_eq!(data.len(), P3G_HEADER_SIZE);

        // Segments
        for _ in 0..num_segments {
            data.extend_from_slice(&0u32.to_le_bytes()); // material_id
            data.extend_from_slice(&0u16.to_le_bytes()); // start_meshlet
            data.extend_from_slice(&num_meshlets.to_le_bytes()); // num_meshlets
            data.extend_from_slice(&0i16.to_le_bytes()); // qbvol_pos x
            data.extend_from_slice(&0i16.to_le_bytes()); // qbvol_pos y
            data.extend_from_slice(&0i16.to_le_bytes()); // qbvol_pos z
            data.extend_from_slice(&32768u16.to_le_bytes()); // qbvol_rad
        }

        let has_bvols = flags & FLAG_BVOLS != 0;
        let has_vcones = flags & FLAG_VCONES != 0;

        // Meshlet descriptors
        for _ in 0..num_meshlets {
            // vidx_packed: offset=0, vtx_count=3
            let vidx_packed: u32 = 0 | (3 << 24);
            data.extend_from_slice(&vidx_packed.to_le_bytes());
            // tidx_packed: offset=0, tri_count=1
            let tidx_packed: u32 = 0 | (1 << 24);
            data.extend_from_slice(&tidx_packed.to_le_bytes());

            if has_bvols {
                data.extend_from_slice(&[0i8 as u8, 0, 0, 127u8]); // bvol
            }
            if has_vcones {
                data.extend_from_slice(&[0i8 as u8, 127u8 as u8, 0, 64u8 as u8]);
                // vcone
            }
        }

        // Pad to 4-byte alignment
        while data.len() % 4 != 0 {
            data.push(0);
        }

        // Vertex index buffer (3 indices: 0, 1, 2 as u16)
        data.extend_from_slice(&0u16.to_le_bytes());
        data.extend_from_slice(&1u16.to_le_bytes());
        data.extend_from_slice(&2u16.to_le_bytes());

        // Pad to 4-byte alignment
        while data.len() % 4 != 0 {
            data.push(0);
        }

        // Triangle index buffer (1 triangle: 0, 1, 2)
        data.extend_from_slice(&[0u8, 1, 2]);

        // Pad to 4-byte alignment
        while data.len() % 4 != 0 {
            data.push(0);
        }

        // Vertex buffer (3 float32 positions)
        let vbuf_offset = data.len() as u32;
        let vbuf_size = 3 * 12; // 3 vertices × 12 bytes (float32 xyz)
        data.extend_from_slice(&[0.0f32, 0.0, 0.0].map(|f| f.to_le_bytes()).concat());
        data.extend_from_slice(&[1.0f32, 0.0, 0.0].map(|f| f.to_le_bytes()).concat());
        data.extend_from_slice(&[0.0f32, 1.0, 0.0].map(|f| f.to_le_bytes()).concat());

        // Patch header: total_file_size, vbuf_offset, vbuf_size
        let total = data.len() as u32;
        data[8..12].copy_from_slice(&total.to_le_bytes());
        data[16..20].copy_from_slice(&vbuf_offset.to_le_bytes());
        data[20..24].copy_from_slice(&(vbuf_size as u32).to_le_bytes());

        data
    }

    #[test]
    fn parse_minimal_p3g() {
        let data = make_minimal_p3g(1, 1, FLAG_BVOLS);
        let file = parse_p3g(&data).expect("parse failed");
        assert_eq!(file.header.version, P3G_VERSION_1_0);
        assert_eq!(file.header.num_meshlets, 1);
        assert_eq!(file.header.num_segments, 1);
        assert_eq!(file.meshlets.len(), 1);
        assert_eq!(file.meshlets[0].vertex_count, 3);
        assert_eq!(file.meshlets[0].triangle_count, 1);
        assert_eq!(file.segments.len(), 1);
    }

    #[test]
    fn reject_bad_magic() {
        let mut data = make_minimal_p3g(1, 1, 0);
        data[0] = b'x';
        assert!(matches!(parse_p3g(&data), Err(P3gError::BadMagic(_))));
    }

    #[test]
    fn reject_tristrips() {
        let data = make_minimal_p3g(1, 1, FLAG_TRISTRIPS);
        assert!(matches!(parse_p3g(&data), Err(P3gError::Tristrips)));
    }

    #[test]
    fn reject_unsupported_version() {
        let mut data = make_minimal_p3g(1, 1, 0);
        data[4..6].copy_from_slice(&0x2000u16.to_le_bytes());
        assert!(matches!(parse_p3g(&data), Err(P3gError::UnsupportedVersion(0x2000))));
    }

    #[test]
    fn reject_too_small() {
        let data = vec![0u8; 10];
        assert!(matches!(parse_p3g(&data), Err(P3gError::TooSmall { .. })));
    }

    #[test]
    fn convert_produces_gpu_meshlets() {
        let data = make_minimal_p3g(1, 1, FLAG_BVOLS);
        let file = parse_p3g(&data).unwrap();
        let result = convert_to_gpu_meshlets(&file, 3, 42);

        assert_eq!(result.meshlets.len(), 1);
        assert_eq!(result.meshlets[0].layer_index, 3);
        assert_eq!(result.meshlets[0].object_id, 42);
        assert_eq!(result.meshlets[0].vertex_count, 3);
        assert_eq!(result.meshlets[0].triangle_count, 1);
        assert_eq!(result.vertices.len(), 3);
        assert_eq!(result.triangles.len(), 3);
    }

    #[test]
    fn load_p3g_convenience() {
        let data = make_minimal_p3g(1, 1, FLAG_BVOLS);
        let result = load_p3g(&data, 0, 1).unwrap();
        assert_eq!(result.stats.meshlet_count, 1);
        assert_eq!(result.stats.segment_count, 1);
    }

    #[test]
    fn bounding_sphere_preserved() {
        let data = make_minimal_p3g(1, 1, 0);
        let file = parse_p3g(&data).unwrap();
        assert_eq!(file.header.bounding_sphere[3], 1.5);
    }

    #[test]
    fn vertex_format_layouts() {
        assert_eq!(vertex_format_layout(1).unwrap(), (12, 0));
        assert_eq!(vertex_format_layout(2).unwrap(), (24, 0));
        assert_eq!(vertex_format_layout(6).unwrap(), (6, 0));
        assert!(vertex_format_layout(99).is_err());
    }

    #[test]
    fn align4_works() {
        assert_eq!(align4(0), 0);
        assert_eq!(align4(1), 4);
        assert_eq!(align4(4), 4);
        assert_eq!(align4(5), 8);
    }
}
