//! Exports the live world to a binary glTF (.glb) document.
//!
//! Walks every entity with a local transform (minus an exclusion set the
//! caller provides, e.g. editor scaffolding) and serializes the scene graph,
//! meshes, materials, textures, samplers, skins, morph targets, lights,
//! cameras, and animation clips into a single self-contained GLB byte
//! buffer. Mesh geometry comes from the CPU-side mesh cache, materials from
//! the material registry, and texture bytes from
//! `world.resources.assets.texture_sources`, so no GPU readback is involved.

use crate::ecs::animation::components::{
    AnimationChannel, AnimationInterpolation, AnimationProperty, AnimationSamplerOutput,
};
use crate::ecs::light::components::{Light, LightType};
use crate::ecs::material::components::{AlphaMode, Material, TextureTransform};
use crate::ecs::mesh::components::Mesh;
use crate::ecs::world::components::{Camera, Projection};
use crate::ecs::world::{Entity, LOCAL_TRANSFORM, World};
use crate::render::wgpu::texture_cache::{SamplerFilter, SamplerSettings, SamplerWrap};
use nalgebra_glm::Quat;
use serde_json::{Map, Value, json};
use std::collections::{BTreeSet, HashMap, HashSet};

const GLB_MAGIC: u32 = 0x4654_6C67;
const GLB_VERSION: u32 = 2;
const GLB_CHUNK_JSON: u32 = 0x4E4F_534A;
const GLB_CHUNK_BIN: u32 = 0x004E_4942;
const TARGET_ARRAY_BUFFER: u32 = 34962;
const TARGET_ELEMENT_ARRAY_BUFFER: u32 = 34963;
const COMPONENT_U16: u32 = 5123;
const COMPONENT_U32: u32 = 5125;
const COMPONENT_F32: u32 = 5126;

#[derive(Default)]
struct ExportContext {
    buffer: Vec<u8>,
    buffer_views: Vec<Value>,
    accessors: Vec<Value>,
    samplers: Vec<Value>,
    sampler_indices: HashMap<String, usize>,
    images: Vec<Value>,
    textures: Vec<Value>,
    texture_indices: HashMap<String, Option<usize>>,
    materials: Vec<Value>,
    material_indices: HashMap<String, Option<usize>>,
    meshes: Vec<Value>,
    mesh_indices: HashMap<(String, Option<String>), Option<usize>>,
    skins: Vec<Value>,
    cameras: Vec<Value>,
    lights: Vec<Value>,
    extensions_used: BTreeSet<String>,
}

/// Serializes every non-excluded entity in the world into a GLB byte buffer.
/// Entities in `excluded_entities` (and their descendants) are skipped.
pub fn export_world_to_glb_bytes(
    world: &World,
    excluded_entities: &HashSet<Entity>,
) -> Result<Vec<u8>, String> {
    let mut context = ExportContext::default();

    let entities = collect_export_entities(world, excluded_entities);
    let entity_to_node: HashMap<Entity, usize> = entities
        .iter()
        .enumerate()
        .map(|(index, entity)| (*entity, index))
        .collect();

    let mut nodes: Vec<Map<String, Value>> = Vec::with_capacity(entities.len());
    let mut node_children: Vec<Vec<usize>> = Vec::with_capacity(entities.len());
    for entity in &entities {
        nodes.push(build_entity_node(&mut context, world, *entity)?);
        node_children.push(Vec::new());
    }

    expand_instanced_meshes(
        &mut context,
        world,
        &entities,
        &mut nodes,
        &mut node_children,
    );
    attach_skins(&mut context, world, &entities, &entity_to_node, &mut nodes);

    let mut root_nodes: Vec<usize> = Vec::new();
    for (node_index, entity) in entities.iter().enumerate() {
        let parent = world
            .core
            .get_parent(*entity)
            .and_then(|parent| parent.0)
            .and_then(|parent_entity| entity_to_node.get(&parent_entity).copied());
        match parent {
            Some(parent_index) => node_children[parent_index].push(node_index),
            None => root_nodes.push(node_index),
        }
    }
    for (node, children) in nodes.iter_mut().zip(&node_children) {
        if !children.is_empty() {
            node.insert(
                "children".to_string(),
                Value::Array(children.iter().map(|index| json!(index)).collect()),
            );
        }
    }

    let animations = build_animations(&mut context, world, &entities, &entity_to_node);

    let (root, binary) = assemble_root(context, nodes, root_nodes, animations);
    encode_glb(root, binary)
}

fn collect_export_entities(world: &World, excluded_entities: &HashSet<Entity>) -> Vec<Entity> {
    let mut entities: Vec<Entity> = Vec::new();
    world
        .core
        .query()
        .with(LOCAL_TRANSFORM)
        .iter(|entity, _, _| entities.push(entity));
    entities.retain(|entity| !has_excluded_ancestor(world, excluded_entities, *entity));
    entities
}

fn has_excluded_ancestor(
    world: &World,
    excluded_entities: &HashSet<Entity>,
    entity: Entity,
) -> bool {
    let mut current = Some(entity);
    while let Some(candidate) = current {
        if excluded_entities.contains(&candidate) {
            return true;
        }
        current = world.core.get_parent(candidate).and_then(|parent| parent.0);
    }
    false
}

fn build_entity_node(
    context: &mut ExportContext,
    world: &World,
    entity: Entity,
) -> Result<Map<String, Value>, String> {
    let mut node = Map::new();

    if let Some(name) = world.core.get_name(entity)
        && !name.0.is_empty()
    {
        node.insert("name".to_string(), json!(name.0));
    }

    if let Some(transform) = world.core.get_local_transform(entity) {
        let translation = transform.translation;
        if translation != nalgebra_glm::Vec3::zeros() {
            node.insert(
                "translation".to_string(),
                json!([translation.x, translation.y, translation.z]),
            );
        }
        let rotation = transform.rotation;
        if !is_identity_quat(&rotation) {
            node.insert(
                "rotation".to_string(),
                json!([rotation.i, rotation.j, rotation.k, rotation.w]),
            );
        }
        let scale = transform.scale;
        if scale != nalgebra_glm::Vec3::new(1.0, 1.0, 1.0) {
            node.insert("scale".to_string(), json!([scale.x, scale.y, scale.z]));
        }
    }

    let visible = world
        .core
        .get_visibility(entity)
        .map(|visibility| visibility.visible)
        .unwrap_or(true);
    if visible && let Some(render_mesh) = world.core.get_render_mesh(entity) {
        let material_name = world
            .core
            .get_material_ref(entity)
            .map(|material_ref| material_ref.name.clone());
        if let Some(mesh_index) =
            resolve_mesh_index(context, world, &render_mesh.name, material_name.as_deref())?
        {
            node.insert("mesh".to_string(), json!(mesh_index));
            if let Some(morph_weights) = world.core.get_morph_weights(entity)
                && !morph_weights.weights.is_empty()
            {
                node.insert("weights".to_string(), json!(morph_weights.weights));
            }
        }
    }

    if let Some(camera) = world.core.get_camera(entity) {
        let camera_index = context.cameras.len();
        context.cameras.push(build_camera(camera, world, entity));
        node.insert("camera".to_string(), json!(camera_index));
    }

    if let Some(light) = world.core.get_light(entity)
        && !matches!(light.light_type, LightType::Area)
    {
        let light_index = context.lights.len();
        context.lights.push(build_light(light));
        context
            .extensions_used
            .insert("KHR_lights_punctual".to_string());
        node.insert(
            "extensions".to_string(),
            json!({ "KHR_lights_punctual": { "light": light_index } }),
        );
    }

    Ok(node)
}

fn is_identity_quat(rotation: &Quat) -> bool {
    rotation.i == 0.0 && rotation.j == 0.0 && rotation.k == 0.0 && rotation.w == 1.0
}

fn expand_instanced_meshes(
    context: &mut ExportContext,
    world: &World,
    entities: &[Entity],
    nodes: &mut Vec<Map<String, Value>>,
    node_children: &mut Vec<Vec<usize>>,
) {
    for (node_index, entity) in entities.iter().enumerate() {
        let Some(instanced) = world.core.get_instanced_mesh(*entity) else {
            continue;
        };
        let material_name = world
            .core
            .get_material_ref(*entity)
            .map(|material_ref| material_ref.name.clone());
        let Ok(Some(mesh_index)) = resolve_mesh_index(
            context,
            world,
            &instanced.mesh_name,
            material_name.as_deref(),
        ) else {
            continue;
        };
        for instance in &instanced.instances {
            let mut instance_node = Map::new();
            instance_node.insert("mesh".to_string(), json!(mesh_index));
            if instance.translation != nalgebra_glm::Vec3::zeros() {
                instance_node.insert(
                    "translation".to_string(),
                    json!([
                        instance.translation.x,
                        instance.translation.y,
                        instance.translation.z
                    ]),
                );
            }
            if !is_identity_quat(&instance.rotation) {
                instance_node.insert(
                    "rotation".to_string(),
                    json!([
                        instance.rotation.i,
                        instance.rotation.j,
                        instance.rotation.k,
                        instance.rotation.w
                    ]),
                );
            }
            if instance.scale != nalgebra_glm::Vec3::new(1.0, 1.0, 1.0) {
                instance_node.insert(
                    "scale".to_string(),
                    json!([instance.scale.x, instance.scale.y, instance.scale.z]),
                );
            }
            let instance_index = nodes.len();
            nodes.push(instance_node);
            node_children.push(Vec::new());
            node_children[node_index].push(instance_index);
        }
    }
}

fn attach_skins(
    context: &mut ExportContext,
    world: &World,
    entities: &[Entity],
    entity_to_node: &HashMap<Entity, usize>,
    nodes: &mut [Map<String, Value>],
) {
    let mut skins: Vec<Value> = Vec::new();
    let mut skin_indices: HashMap<Vec<Entity>, usize> = HashMap::new();

    for (node_index, entity) in entities.iter().enumerate() {
        let Some(skin) = world.core.get_skin(*entity) else {
            continue;
        };
        if !nodes[node_index].contains_key("mesh") {
            continue;
        }
        let joint_nodes: Option<Vec<usize>> = skin
            .joints
            .iter()
            .map(|joint| entity_to_node.get(joint).copied())
            .collect();
        let Some(joint_nodes) = joint_nodes else {
            continue;
        };
        if joint_nodes.is_empty() {
            continue;
        }
        let skin_index = match skin_indices.get(&skin.joints) {
            Some(existing) => *existing,
            None => {
                let mut matrix_floats: Vec<f32> =
                    Vec::with_capacity(skin.inverse_bind_matrices.len() * 16);
                for matrix in skin.inverse_bind_matrices.iter() {
                    matrix_floats.extend_from_slice(matrix.as_slice());
                }
                while matrix_floats.len() < joint_nodes.len() * 16 {
                    matrix_floats.extend_from_slice(nalgebra_glm::Mat4::identity().as_slice());
                }
                matrix_floats.truncate(joint_nodes.len() * 16);
                let matrices_accessor =
                    push_f32_accessor(context, &matrix_floats, "MAT4", None, false);
                let mut skin_json = Map::new();
                skin_json.insert(
                    "joints".to_string(),
                    Value::Array(joint_nodes.iter().map(|index| json!(index)).collect()),
                );
                skin_json.insert("inverseBindMatrices".to_string(), json!(matrices_accessor));
                if let Some(name) = &skin.name {
                    skin_json.insert("name".to_string(), json!(name));
                }
                let new_index = skins.len();
                skins.push(Value::Object(skin_json));
                skin_indices.insert(skin.joints.clone(), new_index);
                new_index
            }
        };
        nodes[node_index].insert("skin".to_string(), json!(skin_index));
    }

    context.skins = skins;
}

fn resolve_mesh_index(
    context: &mut ExportContext,
    world: &World,
    mesh_name: &str,
    material_name: Option<&str>,
) -> Result<Option<usize>, String> {
    let key = (mesh_name.to_string(), material_name.map(str::to_string));
    if let Some(existing) = context.mesh_indices.get(&key) {
        return Ok(*existing);
    }
    let Some(mesh) = crate::ecs::generational_registry::registry_entry_by_name(
        &world.resources.assets.mesh_cache.registry,
        mesh_name,
    ) else {
        context.mesh_indices.insert(key, None);
        return Ok(None);
    };
    if mesh.vertices.is_empty() || mesh.indices.is_empty() {
        context.mesh_indices.insert(key, None);
        return Ok(None);
    }

    let material_index = match material_name {
        Some(name) => resolve_material_index(context, world, name),
        None => None,
    };

    let primitive = build_mesh_primitive(context, mesh, material_index)?;
    let display_name = mesh_name.rsplit("::").next().unwrap_or(mesh_name);
    let mut mesh_json = Map::new();
    mesh_json.insert("name".to_string(), json!(display_name));
    mesh_json.insert("primitives".to_string(), Value::Array(vec![primitive]));
    if let Some(morph_targets) = &mesh.morph_targets
        && !morph_targets.default_weights.is_empty()
    {
        mesh_json.insert("weights".to_string(), json!(morph_targets.default_weights));
    }

    let mesh_index = context.meshes.len();
    context.meshes.push(Value::Object(mesh_json));
    context.mesh_indices.insert(key, Some(mesh_index));
    Ok(Some(mesh_index))
}

fn build_mesh_primitive(
    context: &mut ExportContext,
    mesh: &Mesh,
    material_index: Option<usize>,
) -> Result<Value, String> {
    let vertex_count = mesh.vertices.len();
    let mut positions: Vec<f32> = Vec::with_capacity(vertex_count * 3);
    let mut normals: Vec<f32> = Vec::with_capacity(vertex_count * 3);
    let mut tex_coords_0: Vec<f32> = Vec::with_capacity(vertex_count * 2);
    let mut tex_coords_1: Vec<f32> = Vec::with_capacity(vertex_count * 2);
    let mut tangents: Vec<f32> = Vec::with_capacity(vertex_count * 4);
    let mut colors: Vec<f32> = Vec::with_capacity(vertex_count * 4);
    let mut joint_indices: Vec<u32> = Vec::new();
    let mut joint_weights: Vec<f32> = Vec::new();

    let skinned_vertices = mesh
        .skin_data
        .as_ref()
        .map(|skin_data| &skin_data.skinned_vertices)
        .filter(|skinned| skinned.len() == vertex_count);

    if let Some(skinned) = skinned_vertices {
        joint_indices.reserve(vertex_count * 4);
        joint_weights.reserve(vertex_count * 4);
        for vertex in skinned {
            positions.extend_from_slice(&vertex.position);
            normals.extend_from_slice(&vertex.normal);
            tex_coords_0.extend_from_slice(&vertex.tex_coords);
            tex_coords_1.extend_from_slice(&vertex.tex_coords_1);
            tangents.extend_from_slice(&vertex.tangent);
            colors.extend_from_slice(&vertex.color);
            joint_indices.extend_from_slice(&vertex.joint_indices);
            joint_weights.extend_from_slice(&vertex.joint_weights);
        }
    } else {
        for vertex in &mesh.vertices {
            positions.extend_from_slice(&vertex.position);
            normals.extend_from_slice(&vertex.normal);
            tex_coords_0.extend_from_slice(&vertex.tex_coords);
            tex_coords_1.extend_from_slice(&vertex.tex_coords_1);
            tangents.extend_from_slice(&vertex.tangent);
            colors.extend_from_slice(&vertex.color);
        }
    }

    let mut attributes = Map::new();
    attributes.insert(
        "POSITION".to_string(),
        json!(push_f32_accessor(
            context,
            &positions,
            "VEC3",
            Some(TARGET_ARRAY_BUFFER),
            true,
        )),
    );
    attributes.insert(
        "NORMAL".to_string(),
        json!(push_f32_accessor(
            context,
            &normals,
            "VEC3",
            Some(TARGET_ARRAY_BUFFER),
            false,
        )),
    );
    attributes.insert(
        "TEXCOORD_0".to_string(),
        json!(push_f32_accessor(
            context,
            &tex_coords_0,
            "VEC2",
            Some(TARGET_ARRAY_BUFFER),
            false,
        )),
    );
    if tex_coords_1.iter().any(|value| *value != 0.0) {
        attributes.insert(
            "TEXCOORD_1".to_string(),
            json!(push_f32_accessor(
                context,
                &tex_coords_1,
                "VEC2",
                Some(TARGET_ARRAY_BUFFER),
                false,
            )),
        );
    }
    attributes.insert(
        "TANGENT".to_string(),
        json!(push_f32_accessor(
            context,
            &tangents,
            "VEC4",
            Some(TARGET_ARRAY_BUFFER),
            false,
        )),
    );
    if colors.chunks_exact(4).any(|color| color != [1.0; 4]) {
        attributes.insert(
            "COLOR_0".to_string(),
            json!(push_f32_accessor(
                context,
                &colors,
                "VEC4",
                Some(TARGET_ARRAY_BUFFER),
                false,
            )),
        );
    }
    if !joint_indices.is_empty() {
        let max_joint_index = joint_indices.iter().copied().max().unwrap_or(0);
        if max_joint_index > u16::MAX as u32 {
            return Err(format!(
                "joint index {max_joint_index} exceeds the unsigned short range required by glTF"
            ));
        }
        let joints_u16: Vec<u16> = joint_indices
            .iter()
            .map(|joint_index| *joint_index as u16)
            .collect();
        attributes.insert(
            "JOINTS_0".to_string(),
            json!(push_u16_accessor(
                context,
                &joints_u16,
                "VEC4",
                Some(TARGET_ARRAY_BUFFER),
            )),
        );
        attributes.insert(
            "WEIGHTS_0".to_string(),
            json!(push_f32_accessor(
                context,
                &joint_weights,
                "VEC4",
                Some(TARGET_ARRAY_BUFFER),
                false,
            )),
        );
    }

    let indices_accessor = push_u32_accessor(
        context,
        &mesh.indices,
        "SCALAR",
        Some(TARGET_ELEMENT_ARRAY_BUFFER),
    );

    let mut primitive = Map::new();
    primitive.insert("attributes".to_string(), Value::Object(attributes));
    primitive.insert("indices".to_string(), json!(indices_accessor));
    primitive.insert("mode".to_string(), json!(4));
    if let Some(material_index) = material_index {
        primitive.insert("material".to_string(), json!(material_index));
    }

    if let Some(morph_targets) = &mesh.morph_targets
        && !morph_targets.targets.is_empty()
    {
        let mut targets_json: Vec<Value> = Vec::with_capacity(morph_targets.targets.len());
        for target in &morph_targets.targets {
            let mut target_json = Map::new();
            let position_floats: Vec<f32> = target
                .position_displacements
                .iter()
                .flat_map(|displacement| displacement.iter().copied())
                .collect();
            target_json.insert(
                "POSITION".to_string(),
                json!(push_f32_accessor(
                    context,
                    &position_floats,
                    "VEC3",
                    Some(TARGET_ARRAY_BUFFER),
                    true,
                )),
            );
            if let Some(normal_displacements) = &target.normal_displacements {
                let normal_floats: Vec<f32> = normal_displacements
                    .iter()
                    .flat_map(|displacement| displacement.iter().copied())
                    .collect();
                target_json.insert(
                    "NORMAL".to_string(),
                    json!(push_f32_accessor(
                        context,
                        &normal_floats,
                        "VEC3",
                        Some(TARGET_ARRAY_BUFFER),
                        false,
                    )),
                );
            }
            if let Some(tangent_displacements) = &target.tangent_displacements {
                let tangent_floats: Vec<f32> = tangent_displacements
                    .iter()
                    .flat_map(|displacement| displacement.iter().copied())
                    .collect();
                target_json.insert(
                    "TANGENT".to_string(),
                    json!(push_f32_accessor(
                        context,
                        &tangent_floats,
                        "VEC3",
                        Some(TARGET_ARRAY_BUFFER),
                        false,
                    )),
                );
            }
            targets_json.push(Value::Object(target_json));
        }
        primitive.insert("targets".to_string(), Value::Array(targets_json));
    }

    Ok(Value::Object(primitive))
}

fn resolve_material_index(
    context: &mut ExportContext,
    world: &World,
    material_name: &str,
) -> Option<usize> {
    if let Some(existing) = context.material_indices.get(material_name) {
        return *existing;
    }
    let Some(material) = crate::ecs::generational_registry::registry_entry_by_name(
        &world.resources.assets.material_registry.registry,
        material_name,
    ) else {
        context
            .material_indices
            .insert(material_name.to_string(), None);
        return None;
    };
    let material_json = build_material(context, world, material, material_name);
    let material_index = context.materials.len();
    context.materials.push(material_json);
    context
        .material_indices
        .insert(material_name.to_string(), Some(material_index));
    Some(material_index)
}

fn build_material(
    context: &mut ExportContext,
    world: &World,
    material: &Material,
    material_name: &str,
) -> Value {
    let mut material_json = Map::new();
    let display_name = material_name.rsplit("::").next().unwrap_or(material_name);
    material_json.insert("name".to_string(), json!(display_name));

    let mut pbr = Map::new();
    pbr.insert("baseColorFactor".to_string(), json!(material.base_color));
    if let Some(info) = texture_info(
        context,
        world,
        material.base_texture.as_deref(),
        &material.base_texture_transform,
    ) {
        pbr.insert("baseColorTexture".to_string(), info);
    }
    pbr.insert("metallicFactor".to_string(), json!(material.metallic));
    pbr.insert("roughnessFactor".to_string(), json!(material.roughness));
    if let Some(info) = texture_info(
        context,
        world,
        material.metallic_roughness_texture.as_deref(),
        &material.metallic_roughness_texture_transform,
    ) {
        pbr.insert("metallicRoughnessTexture".to_string(), info);
    }
    material_json.insert("pbrMetallicRoughness".to_string(), Value::Object(pbr));

    if let Some(mut info) = texture_info(
        context,
        world,
        material.normal_texture.as_deref(),
        &material.normal_texture_transform,
    ) {
        if material.normal_scale != 1.0
            && let Some(object) = info.as_object_mut()
        {
            object.insert("scale".to_string(), json!(material.normal_scale));
        }
        material_json.insert("normalTexture".to_string(), info);
    }

    if let Some(mut info) = texture_info(
        context,
        world,
        material.occlusion_texture.as_deref(),
        &material.occlusion_texture_transform,
    ) {
        if material.occlusion_strength != 1.0
            && let Some(object) = info.as_object_mut()
        {
            object.insert("strength".to_string(), json!(material.occlusion_strength));
        }
        material_json.insert("occlusionTexture".to_string(), info);
    }

    if material.emissive_factor != [0.0, 0.0, 0.0] {
        material_json.insert(
            "emissiveFactor".to_string(),
            json!(material.emissive_factor),
        );
    }
    if let Some(info) = texture_info(
        context,
        world,
        material.emissive_texture.as_deref(),
        &material.emissive_texture_transform,
    ) {
        material_json.insert("emissiveTexture".to_string(), info);
    }

    match material.alpha_mode {
        AlphaMode::Opaque => {}
        AlphaMode::Mask => {
            material_json.insert("alphaMode".to_string(), json!("MASK"));
            material_json.insert("alphaCutoff".to_string(), json!(material.alpha_cutoff));
        }
        AlphaMode::Blend => {
            material_json.insert("alphaMode".to_string(), json!("BLEND"));
        }
    }
    if material.double_sided {
        material_json.insert("doubleSided".to_string(), json!(true));
    }

    let extensions = build_material_extensions(context, world, material);
    if !extensions.is_empty() {
        material_json.insert("extensions".to_string(), Value::Object(extensions));
    }

    Value::Object(material_json)
}

fn build_material_extensions(
    context: &mut ExportContext,
    world: &World,
    material: &Material,
) -> Map<String, Value> {
    let mut extensions = Map::new();

    if material.unlit {
        context
            .extensions_used
            .insert("KHR_materials_unlit".to_string());
        extensions.insert("KHR_materials_unlit".to_string(), json!({}));
    }

    if material.emissive_strength != 1.0 {
        context
            .extensions_used
            .insert("KHR_materials_emissive_strength".to_string());
        extensions.insert(
            "KHR_materials_emissive_strength".to_string(),
            json!({ "emissiveStrength": material.emissive_strength }),
        );
    }

    if material.ior != 1.5 {
        context
            .extensions_used
            .insert("KHR_materials_ior".to_string());
        extensions.insert(
            "KHR_materials_ior".to_string(),
            json!({ "ior": material.ior }),
        );
    }

    let transmission_texture = texture_info(
        context,
        world,
        material.transmission_texture.as_deref(),
        &material.transmission_texture_transform,
    );
    if material.transmission_factor > 0.0 || transmission_texture.is_some() {
        context
            .extensions_used
            .insert("KHR_materials_transmission".to_string());
        let mut transmission = Map::new();
        transmission.insert(
            "transmissionFactor".to_string(),
            json!(material.transmission_factor),
        );
        if let Some(info) = transmission_texture {
            transmission.insert("transmissionTexture".to_string(), info);
        }
        extensions.insert(
            "KHR_materials_transmission".to_string(),
            Value::Object(transmission),
        );
    }

    let thickness_texture = texture_info(
        context,
        world,
        material.thickness_texture.as_deref(),
        &material.thickness_texture_transform,
    );
    if material.thickness > 0.0 || thickness_texture.is_some() {
        context
            .extensions_used
            .insert("KHR_materials_volume".to_string());
        let mut volume = Map::new();
        volume.insert("thicknessFactor".to_string(), json!(material.thickness));
        if let Some(info) = thickness_texture {
            volume.insert("thicknessTexture".to_string(), info);
        }
        if material.attenuation_color != [1.0, 1.0, 1.0] {
            volume.insert(
                "attenuationColor".to_string(),
                json!(material.attenuation_color),
            );
        }
        if material.attenuation_distance > 0.0 && material.attenuation_distance.is_finite() {
            volume.insert(
                "attenuationDistance".to_string(),
                json!(material.attenuation_distance),
            );
        }
        extensions.insert("KHR_materials_volume".to_string(), Value::Object(volume));
    }

    let specular_texture = texture_info(
        context,
        world,
        material.specular_texture.as_deref(),
        &material.specular_texture_transform,
    );
    let specular_color_texture = texture_info(
        context,
        world,
        material.specular_color_texture.as_deref(),
        &material.specular_color_texture_transform,
    );
    if material.specular_factor != 1.0
        || material.specular_color_factor != [1.0, 1.0, 1.0]
        || specular_texture.is_some()
        || specular_color_texture.is_some()
    {
        context
            .extensions_used
            .insert("KHR_materials_specular".to_string());
        let mut specular = Map::new();
        if material.specular_factor != 1.0 {
            specular.insert(
                "specularFactor".to_string(),
                json!(material.specular_factor),
            );
        }
        if material.specular_color_factor != [1.0, 1.0, 1.0] {
            specular.insert(
                "specularColorFactor".to_string(),
                json!(material.specular_color_factor),
            );
        }
        if let Some(info) = specular_texture {
            specular.insert("specularTexture".to_string(), info);
        }
        if let Some(info) = specular_color_texture {
            specular.insert("specularColorTexture".to_string(), info);
        }
        extensions.insert(
            "KHR_materials_specular".to_string(),
            Value::Object(specular),
        );
    }

    let diffuse_transmission_color_texture = texture_info(
        context,
        world,
        material.diffuse_transmission_color_texture.as_deref(),
        &material.diffuse_transmission_color_texture_transform,
    );
    let diffuse_transmission_texture = texture_info(
        context,
        world,
        material
            .diffuse_transmission_texture
            .as_deref()
            .or(material.diffuse_transmission_color_texture.as_deref()),
        &material.diffuse_transmission_texture_transform,
    );
    if material.diffuse_transmission_factor > 0.0 {
        context
            .extensions_used
            .insert("KHR_materials_diffuse_transmission".to_string());
        let mut diffuse_transmission = Map::new();
        diffuse_transmission.insert(
            "diffuseTransmissionFactor".to_string(),
            json!(material.diffuse_transmission_factor),
        );
        if let Some(info) = diffuse_transmission_texture {
            diffuse_transmission.insert("diffuseTransmissionTexture".to_string(), info);
        }
        if material.diffuse_transmission_color_factor != [1.0, 1.0, 1.0] {
            diffuse_transmission.insert(
                "diffuseTransmissionColorFactor".to_string(),
                json!(material.diffuse_transmission_color_factor),
            );
        }
        if let Some(info) = diffuse_transmission_color_texture {
            diffuse_transmission.insert("diffuseTransmissionColorTexture".to_string(), info);
        }
        extensions.insert(
            "KHR_materials_diffuse_transmission".to_string(),
            Value::Object(diffuse_transmission),
        );
    }

    if material.dispersion > 0.0 {
        context
            .extensions_used
            .insert("KHR_materials_dispersion".to_string());
        extensions.insert(
            "KHR_materials_dispersion".to_string(),
            json!({ "dispersion": material.dispersion }),
        );
    }

    let anisotropy_texture = texture_info(
        context,
        world,
        material.anisotropy_texture.as_deref(),
        &material.anisotropy_texture_transform,
    );
    if material.anisotropy_strength > 0.0 || anisotropy_texture.is_some() {
        context
            .extensions_used
            .insert("KHR_materials_anisotropy".to_string());
        let mut anisotropy = Map::new();
        anisotropy.insert(
            "anisotropyStrength".to_string(),
            json!(material.anisotropy_strength),
        );
        if material.anisotropy_rotation != 0.0 {
            anisotropy.insert(
                "anisotropyRotation".to_string(),
                json!(material.anisotropy_rotation),
            );
        }
        if let Some(info) = anisotropy_texture {
            anisotropy.insert("anisotropyTexture".to_string(), info);
        }
        extensions.insert(
            "KHR_materials_anisotropy".to_string(),
            Value::Object(anisotropy),
        );
    }

    let iridescence_texture = texture_info(
        context,
        world,
        material.iridescence_texture.as_deref(),
        &material.iridescence_texture_transform,
    );
    let iridescence_thickness_texture = texture_info(
        context,
        world,
        material
            .iridescence_thickness_texture
            .as_deref()
            .or(material.iridescence_texture.as_deref()),
        &material.iridescence_thickness_texture_transform,
    );
    if material.iridescence_factor > 0.0 || iridescence_texture.is_some() {
        context
            .extensions_used
            .insert("KHR_materials_iridescence".to_string());
        let mut iridescence = Map::new();
        iridescence.insert(
            "iridescenceFactor".to_string(),
            json!(material.iridescence_factor),
        );
        if material.iridescence_ior != 1.3 {
            iridescence.insert(
                "iridescenceIor".to_string(),
                json!(material.iridescence_ior),
            );
        }
        if material.iridescence_thickness_minimum != 100.0 {
            iridescence.insert(
                "iridescenceThicknessMinimum".to_string(),
                json!(material.iridescence_thickness_minimum),
            );
        }
        if material.iridescence_thickness_maximum != 400.0 {
            iridescence.insert(
                "iridescenceThicknessMaximum".to_string(),
                json!(material.iridescence_thickness_maximum),
            );
        }
        if let Some(info) = iridescence_texture {
            iridescence.insert("iridescenceTexture".to_string(), info);
        }
        if let Some(info) = iridescence_thickness_texture {
            iridescence.insert("iridescenceThicknessTexture".to_string(), info);
        }
        extensions.insert(
            "KHR_materials_iridescence".to_string(),
            Value::Object(iridescence),
        );
    }

    let sheen_color_texture = texture_info(
        context,
        world,
        material.sheen_color_texture.as_deref(),
        &material.sheen_color_texture_transform,
    );
    let sheen_roughness_texture = texture_info(
        context,
        world,
        material
            .sheen_roughness_texture
            .as_deref()
            .or(material.sheen_color_texture.as_deref()),
        &material.sheen_roughness_texture_transform,
    );
    if material.sheen_color_factor != [0.0, 0.0, 0.0] || sheen_color_texture.is_some() {
        context
            .extensions_used
            .insert("KHR_materials_sheen".to_string());
        let mut sheen = Map::new();
        sheen.insert(
            "sheenColorFactor".to_string(),
            json!(material.sheen_color_factor),
        );
        sheen.insert(
            "sheenRoughnessFactor".to_string(),
            json!(material.sheen_roughness_factor),
        );
        if let Some(info) = sheen_color_texture {
            sheen.insert("sheenColorTexture".to_string(), info);
        }
        if let Some(info) = sheen_roughness_texture {
            sheen.insert("sheenRoughnessTexture".to_string(), info);
        }
        extensions.insert("KHR_materials_sheen".to_string(), Value::Object(sheen));
    }

    let clearcoat_texture = texture_info(
        context,
        world,
        material.clearcoat_texture.as_deref(),
        &material.clearcoat_texture_transform,
    );
    let clearcoat_roughness_texture = texture_info(
        context,
        world,
        material.clearcoat_roughness_texture.as_deref(),
        &material.clearcoat_roughness_texture_transform,
    );
    let clearcoat_normal_texture = texture_info(
        context,
        world,
        material.clearcoat_normal_texture.as_deref(),
        &material.clearcoat_normal_texture_transform,
    );
    if material.clearcoat_factor > 0.0 || clearcoat_texture.is_some() {
        context
            .extensions_used
            .insert("KHR_materials_clearcoat".to_string());
        let mut clearcoat = Map::new();
        clearcoat.insert(
            "clearcoatFactor".to_string(),
            json!(material.clearcoat_factor),
        );
        clearcoat.insert(
            "clearcoatRoughnessFactor".to_string(),
            json!(material.clearcoat_roughness_factor),
        );
        if let Some(info) = clearcoat_texture {
            clearcoat.insert("clearcoatTexture".to_string(), info);
        }
        if let Some(info) = clearcoat_roughness_texture {
            clearcoat.insert("clearcoatRoughnessTexture".to_string(), info);
        }
        if let Some(mut info) = clearcoat_normal_texture {
            if material.clearcoat_normal_scale != 1.0
                && let Some(object) = info.as_object_mut()
            {
                object.insert("scale".to_string(), json!(material.clearcoat_normal_scale));
            }
            clearcoat.insert("clearcoatNormalTexture".to_string(), info);
        }
        extensions.insert(
            "KHR_materials_clearcoat".to_string(),
            Value::Object(clearcoat),
        );
    }

    extensions
}

fn texture_info(
    context: &mut ExportContext,
    world: &World,
    texture_name: Option<&str>,
    transform: &TextureTransform,
) -> Option<Value> {
    let texture_name = texture_name?;
    let texture_index = resolve_texture_index(context, world, texture_name)?;
    let mut info = Map::new();
    info.insert("index".to_string(), json!(texture_index));
    if transform.uv_set != 0 {
        info.insert("texCoord".to_string(), json!(transform.uv_set));
    }
    if *transform != TextureTransform::IDENTITY {
        context
            .extensions_used
            .insert("KHR_texture_transform".to_string());
        let mut transform_json = Map::new();
        if transform.offset != [0.0, 0.0] {
            transform_json.insert("offset".to_string(), json!(transform.offset));
        }
        if transform.rotation != 0.0 {
            transform_json.insert("rotation".to_string(), json!(transform.rotation));
        }
        if transform.scale != [1.0, 1.0] {
            transform_json.insert("scale".to_string(), json!(transform.scale));
        }
        if transform.uv_set != 0 {
            transform_json.insert("texCoord".to_string(), json!(transform.uv_set));
        }
        info.insert(
            "extensions".to_string(),
            json!({ "KHR_texture_transform": Value::Object(transform_json) }),
        );
    }
    Some(Value::Object(info))
}

fn resolve_texture_index(
    context: &mut ExportContext,
    world: &World,
    texture_name: &str,
) -> Option<usize> {
    if let Some(existing) = context.texture_indices.get(texture_name) {
        return *existing;
    }
    let Some(source) = world.resources.assets.texture_sources.get(texture_name) else {
        tracing::warn!("glb export: no source bytes for texture '{texture_name}', dropping it");
        context
            .texture_indices
            .insert(texture_name.to_string(), None);
        return None;
    };
    let png_bytes = match &source.data {
        crate::ecs::asset_state::TextureSourceData::Png(bytes) => bytes.clone(),
        crate::ecs::asset_state::TextureSourceData::Rgba {
            rgba,
            width,
            height,
        } => match encode_rgba_to_png(rgba, *width, *height) {
            Ok(bytes) => bytes,
            Err(error) => {
                tracing::warn!("glb export: failed to encode texture '{texture_name}': {error}");
                context
                    .texture_indices
                    .insert(texture_name.to_string(), None);
                return None;
            }
        },
    };

    let view_index = push_buffer_view(context, &png_bytes, None);
    let image_index = context.images.len();
    let display_name = texture_name.rsplit("::").next().unwrap_or(texture_name);
    context.images.push(json!({
        "bufferView": view_index,
        "mimeType": "image/png",
        "name": display_name,
    }));

    let sampler_index = resolve_sampler_index(context, &source.sampler);
    let texture_index = context.textures.len();
    context.textures.push(json!({
        "source": image_index,
        "sampler": sampler_index,
        "name": display_name,
    }));
    context
        .texture_indices
        .insert(texture_name.to_string(), Some(texture_index));
    Some(texture_index)
}

fn encode_rgba_to_png(rgba: &[u8], width: u32, height: u32) -> Result<Vec<u8>, String> {
    let image_buffer = image::RgbaImage::from_raw(width, height, rgba.to_vec())
        .ok_or_else(|| "pixel data does not match dimensions".to_string())?;
    let mut png_bytes: Vec<u8> = Vec::new();
    image::DynamicImage::ImageRgba8(image_buffer)
        .write_to(
            &mut std::io::Cursor::new(&mut png_bytes),
            image::ImageFormat::Png,
        )
        .map_err(|error| error.to_string())?;
    Ok(png_bytes)
}

fn resolve_sampler_index(context: &mut ExportContext, sampler: &SamplerSettings) -> usize {
    let signature = sampler.signature();
    if let Some(existing) = context.sampler_indices.get(&signature) {
        return *existing;
    }
    let wrap_mode = |wrap: SamplerWrap| match wrap {
        SamplerWrap::Repeat => 10497,
        SamplerWrap::MirroredRepeat => 33648,
        SamplerWrap::ClampToEdge => 33071,
    };
    let mag_filter = match sampler.mag_filter {
        SamplerFilter::Nearest => 9728,
        SamplerFilter::Linear => 9729,
    };
    let min_filter = match (sampler.min_filter, sampler.mipmap_filter) {
        (SamplerFilter::Nearest, SamplerFilter::Nearest) => 9984,
        (SamplerFilter::Linear, SamplerFilter::Nearest) => 9985,
        (SamplerFilter::Nearest, SamplerFilter::Linear) => 9986,
        (SamplerFilter::Linear, SamplerFilter::Linear) => 9987,
    };
    let sampler_index = context.samplers.len();
    context.samplers.push(json!({
        "magFilter": mag_filter,
        "minFilter": min_filter,
        "wrapS": wrap_mode(sampler.wrap_u),
        "wrapT": wrap_mode(sampler.wrap_v),
    }));
    context.sampler_indices.insert(signature, sampler_index);
    sampler_index
}

fn build_camera(camera: &Camera, world: &World, entity: Entity) -> Value {
    let name = world
        .core
        .get_name(entity)
        .map(|name| name.0.clone())
        .filter(|name| !name.is_empty());
    let mut camera_json = Map::new();
    if let Some(name) = name {
        camera_json.insert("name".to_string(), json!(name));
    }
    match &camera.projection {
        Projection::Perspective(perspective) => {
            camera_json.insert("type".to_string(), json!("perspective"));
            let mut perspective_json = Map::new();
            if let Some(aspect_ratio) = perspective.aspect_ratio {
                perspective_json.insert("aspectRatio".to_string(), json!(aspect_ratio));
            }
            perspective_json.insert("yfov".to_string(), json!(perspective.y_fov_rad));
            perspective_json.insert("znear".to_string(), json!(perspective.z_near.max(1.0e-4)));
            if let Some(z_far) = perspective.z_far {
                perspective_json.insert("zfar".to_string(), json!(z_far));
            }
            camera_json.insert("perspective".to_string(), Value::Object(perspective_json));
        }
        Projection::Orthographic(orthographic) => {
            camera_json.insert("type".to_string(), json!("orthographic"));
            camera_json.insert(
                "orthographic".to_string(),
                json!({
                    "xmag": orthographic.x_mag,
                    "ymag": orthographic.y_mag,
                    "zfar": orthographic.z_far,
                    "znear": orthographic.z_near,
                }),
            );
        }
    }
    Value::Object(camera_json)
}

fn build_light(light: &Light) -> Value {
    let mut light_json = Map::new();
    let light_type = match light.light_type {
        LightType::Directional => "directional",
        LightType::Point => "point",
        LightType::Spot => "spot",
        LightType::Area => unreachable!(),
    };
    light_json.insert("type".to_string(), json!(light_type));
    light_json.insert(
        "color".to_string(),
        json!([light.color.x, light.color.y, light.color.z]),
    );
    light_json.insert("intensity".to_string(), json!(light.intensity));
    if !matches!(light.light_type, LightType::Directional) && light.range > 0.0 {
        light_json.insert("range".to_string(), json!(light.range));
    }
    if matches!(light.light_type, LightType::Spot) {
        light_json.insert(
            "spot".to_string(),
            json!({
                "innerConeAngle": light.inner_cone_angle,
                "outerConeAngle": light.outer_cone_angle,
            }),
        );
    }
    Value::Object(light_json)
}

fn build_animations(
    context: &mut ExportContext,
    world: &World,
    entities: &[Entity],
    entity_to_node: &HashMap<Entity, usize>,
) -> Vec<Value> {
    let mut animations: Vec<Value> = Vec::new();
    for entity in entities {
        let Some(player) = world.core.get_animation_player(*entity) else {
            continue;
        };
        for clip in &player.clips {
            let mut channels: Vec<Value> = Vec::new();
            let mut samplers: Vec<Value> = Vec::new();
            for channel in &clip.channels {
                let Some(target_entity) = player.resolve_target_entity(channel) else {
                    continue;
                };
                let Some(node_index) = entity_to_node.get(&target_entity).copied() else {
                    continue;
                };
                let Some(sampler_json) = build_animation_sampler(context, channel) else {
                    continue;
                };
                let sampler_index = samplers.len();
                samplers.push(sampler_json);
                let path = match channel.target_property {
                    AnimationProperty::Translation => "translation",
                    AnimationProperty::Rotation => "rotation",
                    AnimationProperty::Scale => "scale",
                    AnimationProperty::MorphWeights => "weights",
                };
                channels.push(json!({
                    "sampler": sampler_index,
                    "target": { "node": node_index, "path": path },
                }));
            }
            if channels.is_empty() {
                continue;
            }
            animations.push(json!({
                "name": clip.name,
                "channels": channels,
                "samplers": samplers,
            }));
        }
    }
    animations
}

fn build_animation_sampler(
    context: &mut ExportContext,
    channel: &AnimationChannel,
) -> Option<Value> {
    let sampler = &channel.sampler;
    if sampler.input.is_empty() {
        return None;
    }
    let input_accessor = push_f32_accessor(context, &sampler.input, "SCALAR", None, true);

    let (output_floats, element_type) = match &sampler.output {
        AnimationSamplerOutput::Vec3(values) => {
            let floats: Vec<f32> = values
                .iter()
                .flat_map(|value| [value.x, value.y, value.z])
                .collect();
            (floats, "VEC3")
        }
        AnimationSamplerOutput::Quat(values) => {
            let floats: Vec<f32> = values
                .iter()
                .flat_map(|value| [value.i, value.j, value.k, value.w])
                .collect();
            (floats, "VEC4")
        }
        AnimationSamplerOutput::Weights(values) => {
            let floats: Vec<f32> = values.iter().flatten().copied().collect();
            (floats, "SCALAR")
        }
        AnimationSamplerOutput::CubicSplineVec3 {
            values,
            in_tangents,
            out_tangents,
        } => {
            let mut floats: Vec<f32> = Vec::with_capacity(values.len() * 9);
            for (index, value) in values.iter().enumerate() {
                let in_tangent = in_tangents.get(index)?;
                let out_tangent = out_tangents.get(index)?;
                floats.extend_from_slice(&[in_tangent.x, in_tangent.y, in_tangent.z]);
                floats.extend_from_slice(&[value.x, value.y, value.z]);
                floats.extend_from_slice(&[out_tangent.x, out_tangent.y, out_tangent.z]);
            }
            (floats, "VEC3")
        }
        AnimationSamplerOutput::CubicSplineQuat {
            values,
            in_tangents,
            out_tangents,
        } => {
            let mut floats: Vec<f32> = Vec::with_capacity(values.len() * 12);
            for (index, value) in values.iter().enumerate() {
                let in_tangent = in_tangents.get(index)?;
                let out_tangent = out_tangents.get(index)?;
                floats.extend_from_slice(&[in_tangent.i, in_tangent.j, in_tangent.k, in_tangent.w]);
                floats.extend_from_slice(&[value.i, value.j, value.k, value.w]);
                floats.extend_from_slice(&[
                    out_tangent.i,
                    out_tangent.j,
                    out_tangent.k,
                    out_tangent.w,
                ]);
            }
            (floats, "VEC4")
        }
        AnimationSamplerOutput::CubicSplineWeights {
            values,
            in_tangents,
            out_tangents,
        } => {
            let mut floats: Vec<f32> = Vec::new();
            for (index, value) in values.iter().enumerate() {
                floats.extend(in_tangents.get(index)?.iter().copied());
                floats.extend(value.iter().copied());
                floats.extend(out_tangents.get(index)?.iter().copied());
            }
            (floats, "SCALAR")
        }
    };
    if output_floats.is_empty() {
        return None;
    }
    let output_accessor = push_f32_accessor(context, &output_floats, element_type, None, false);

    let interpolation = match sampler.interpolation {
        AnimationInterpolation::Linear => "LINEAR",
        AnimationInterpolation::Step => "STEP",
        AnimationInterpolation::CubicSpline => "CUBICSPLINE",
    };
    Some(json!({
        "input": input_accessor,
        "output": output_accessor,
        "interpolation": interpolation,
    }))
}

fn assemble_root(
    context: ExportContext,
    nodes: Vec<Map<String, Value>>,
    root_nodes: Vec<usize>,
    animations: Vec<Value>,
) -> (Map<String, Value>, Vec<u8>) {
    let mut root = Map::new();
    root.insert(
        "asset".to_string(),
        json!({ "version": "2.0", "generator": "nightshade" }),
    );

    if !root_nodes.is_empty() {
        let mut scene = Map::new();
        scene.insert(
            "nodes".to_string(),
            Value::Array(root_nodes.iter().map(|index| json!(index)).collect()),
        );
        root.insert("scene".to_string(), json!(0));
        root.insert(
            "scenes".to_string(),
            Value::Array(vec![Value::Object(scene)]),
        );
    }

    if !nodes.is_empty() {
        root.insert(
            "nodes".to_string(),
            Value::Array(nodes.into_iter().map(Value::Object).collect()),
        );
    }
    if !context.meshes.is_empty() {
        root.insert("meshes".to_string(), Value::Array(context.meshes));
    }
    if !context.materials.is_empty() {
        root.insert("materials".to_string(), Value::Array(context.materials));
    }
    if !context.textures.is_empty() {
        root.insert("textures".to_string(), Value::Array(context.textures));
    }
    if !context.images.is_empty() {
        root.insert("images".to_string(), Value::Array(context.images));
    }
    if !context.samplers.is_empty() {
        root.insert("samplers".to_string(), Value::Array(context.samplers));
    }
    if !context.skins.is_empty() {
        root.insert("skins".to_string(), Value::Array(context.skins));
    }
    if !context.cameras.is_empty() {
        root.insert("cameras".to_string(), Value::Array(context.cameras));
    }
    if !animations.is_empty() {
        root.insert("animations".to_string(), Value::Array(animations));
    }
    if !context.lights.is_empty() {
        root.insert(
            "extensions".to_string(),
            json!({ "KHR_lights_punctual": { "lights": context.lights } }),
        );
    }
    if !context.accessors.is_empty() {
        root.insert("accessors".to_string(), Value::Array(context.accessors));
    }
    if !context.buffer_views.is_empty() {
        root.insert(
            "bufferViews".to_string(),
            Value::Array(context.buffer_views),
        );
    }
    if !context.buffer.is_empty() {
        root.insert(
            "buffers".to_string(),
            json!([{ "byteLength": context.buffer.len() }]),
        );
    }
    if !context.extensions_used.is_empty() {
        root.insert(
            "extensionsUsed".to_string(),
            Value::Array(
                context
                    .extensions_used
                    .iter()
                    .map(|name| json!(name))
                    .collect(),
            ),
        );
    }

    (root, context.buffer)
}

fn encode_glb(root: Map<String, Value>, mut binary: Vec<u8>) -> Result<Vec<u8>, String> {
    let json_string =
        serde_json::to_string(&Value::Object(root)).map_err(|error| error.to_string())?;
    let mut json_bytes = json_string.into_bytes();
    while !json_bytes.len().is_multiple_of(4) {
        json_bytes.push(b' ');
    }
    while !binary.len().is_multiple_of(4) {
        binary.push(0);
    }

    let mut total_length = 12 + 8 + json_bytes.len();
    if !binary.is_empty() {
        total_length += 8 + binary.len();
    }

    let mut output = Vec::with_capacity(total_length);
    output.extend_from_slice(&GLB_MAGIC.to_le_bytes());
    output.extend_from_slice(&GLB_VERSION.to_le_bytes());
    output.extend_from_slice(&(total_length as u32).to_le_bytes());
    output.extend_from_slice(&(json_bytes.len() as u32).to_le_bytes());
    output.extend_from_slice(&GLB_CHUNK_JSON.to_le_bytes());
    output.extend_from_slice(&json_bytes);
    if !binary.is_empty() {
        output.extend_from_slice(&(binary.len() as u32).to_le_bytes());
        output.extend_from_slice(&GLB_CHUNK_BIN.to_le_bytes());
        output.extend_from_slice(&binary);
    }
    Ok(output)
}

fn push_buffer_view(context: &mut ExportContext, bytes: &[u8], target: Option<u32>) -> usize {
    while !context.buffer.len().is_multiple_of(4) {
        context.buffer.push(0);
    }
    let byte_offset = context.buffer.len();
    context.buffer.extend_from_slice(bytes);
    let mut view = Map::new();
    view.insert("buffer".to_string(), json!(0));
    view.insert("byteOffset".to_string(), json!(byte_offset));
    view.insert("byteLength".to_string(), json!(bytes.len()));
    if let Some(target) = target {
        view.insert("target".to_string(), json!(target));
    }
    let view_index = context.buffer_views.len();
    context.buffer_views.push(Value::Object(view));
    view_index
}

fn element_component_count(element_type: &str) -> usize {
    match element_type {
        "SCALAR" => 1,
        "VEC2" => 2,
        "VEC3" => 3,
        "VEC4" => 4,
        "MAT4" => 16,
        _ => 1,
    }
}

fn push_f32_accessor(
    context: &mut ExportContext,
    values: &[f32],
    element_type: &str,
    target: Option<u32>,
    with_min_max: bool,
) -> usize {
    let components = element_component_count(element_type);
    let count = values.len() / components;
    let bytes: Vec<u8> = values
        .iter()
        .flat_map(|value| value.to_le_bytes())
        .collect();
    let view_index = push_buffer_view(context, &bytes, target);
    let mut accessor = Map::new();
    accessor.insert("bufferView".to_string(), json!(view_index));
    accessor.insert("componentType".to_string(), json!(COMPONENT_F32));
    accessor.insert("count".to_string(), json!(count));
    accessor.insert("type".to_string(), json!(element_type));
    if with_min_max && count > 0 {
        let (minimums, maximums) = component_min_max(values, components);
        accessor.insert("min".to_string(), json!(minimums));
        accessor.insert("max".to_string(), json!(maximums));
    }
    let accessor_index = context.accessors.len();
    context.accessors.push(Value::Object(accessor));
    accessor_index
}

fn push_u32_accessor(
    context: &mut ExportContext,
    values: &[u32],
    element_type: &str,
    target: Option<u32>,
) -> usize {
    let components = element_component_count(element_type);
    let count = values.len() / components;
    let bytes: Vec<u8> = values
        .iter()
        .flat_map(|value| value.to_le_bytes())
        .collect();
    let view_index = push_buffer_view(context, &bytes, target);
    let accessor_index = context.accessors.len();
    context.accessors.push(json!({
        "bufferView": view_index,
        "componentType": COMPONENT_U32,
        "count": count,
        "type": element_type,
    }));
    accessor_index
}

fn push_u16_accessor(
    context: &mut ExportContext,
    values: &[u16],
    element_type: &str,
    target: Option<u32>,
) -> usize {
    let components = element_component_count(element_type);
    let count = values.len() / components;
    let bytes: Vec<u8> = values
        .iter()
        .flat_map(|value| value.to_le_bytes())
        .collect();
    let view_index = push_buffer_view(context, &bytes, target);
    let accessor_index = context.accessors.len();
    context.accessors.push(json!({
        "bufferView": view_index,
        "componentType": COMPONENT_U16,
        "count": count,
        "type": element_type,
    }));
    accessor_index
}

fn component_min_max(values: &[f32], components: usize) -> (Vec<f32>, Vec<f32>) {
    let mut minimums = vec![f32::MAX; components];
    let mut maximums = vec![f32::MIN; components];
    for element in values.chunks_exact(components) {
        for (component_index, value) in element.iter().enumerate() {
            minimums[component_index] = minimums[component_index].min(*value);
            maximums[component_index] = maximums[component_index].max(*value);
        }
    }
    (minimums, maximums)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ecs::asset_state::{TextureSourceBytes, TextureSourceData};
    use crate::ecs::generational_registry::registry_entry_by_name_mut;
    use crate::ecs::world::commands::spawn_mesh_at;
    use nalgebra_glm::Vec3;

    #[test]
    fn export_produces_a_valid_glb_with_mesh_material_and_texture() {
        let mut world = World::default();
        let entity = spawn_mesh_at(
            &mut world,
            "Cube",
            Vec3::new(1.0, 2.0, 3.0),
            Vec3::new(1.0, 1.0, 1.0),
        );

        let material_name = world
            .core
            .get_material_ref(entity)
            .map(|material_ref| material_ref.name.clone())
            .expect("spawned mesh has a material reference");
        let material = registry_entry_by_name_mut(
            &mut world.resources.assets.material_registry.registry,
            &material_name,
        )
        .expect("spawned material is registered");
        material.base_texture = Some("test_base".to_string());

        world.resources.assets.texture_sources.insert(
            "test_base".to_string(),
            TextureSourceBytes {
                data: TextureSourceData::Rgba {
                    rgba: vec![255, 128, 64, 255],
                    width: 1,
                    height: 1,
                },
                usage: crate::render::wgpu::texture_cache::TextureUsage::Color,
                sampler: SamplerSettings::DEFAULT,
            },
        );

        let bytes = export_world_to_glb_bytes(&world, &HashSet::new()).expect("export succeeds");
        let document = gltf::Gltf::from_slice(&bytes).expect("exported GLB parses and validates");

        assert_eq!(document.meshes().count(), 1);
        assert_eq!(
            document.materials().filter(|m| m.index().is_some()).count(),
            1
        );
        assert_eq!(document.images().count(), 1);
        assert_eq!(document.textures().count(), 1);
        assert_eq!(document.samplers().count(), 1);

        let node = document
            .nodes()
            .find(|node| node.mesh().is_some())
            .expect("a node references the mesh");
        let (translation, _, _) = node.transform().decomposed();
        assert_eq!(translation, [1.0, 2.0, 3.0]);

        let primitive = document
            .meshes()
            .next()
            .expect("mesh present")
            .primitives()
            .next()
            .expect("primitive present");
        assert!(primitive.material().index().is_some());
        assert!(
            primitive
                .attributes()
                .any(|(semantic, _)| semantic == gltf::Semantic::Positions)
        );
        assert!(primitive.indices().is_some());
    }

    #[test]
    fn excluded_entities_and_descendants_are_skipped() {
        let mut world = World::default();
        let parent = spawn_mesh_at(&mut world, "Cube", Vec3::zeros(), Vec3::new(1.0, 1.0, 1.0));
        let child = spawn_mesh_at(
            &mut world,
            "Cube",
            Vec3::new(2.0, 0.0, 0.0),
            Vec3::new(1.0, 1.0, 1.0),
        );
        world.core.add_components(child, crate::ecs::world::PARENT);
        world.core.set_parent(
            child,
            crate::ecs::transform::components::Parent(Some(parent)),
        );

        let mut excluded = HashSet::new();
        excluded.insert(parent);
        let bytes = export_world_to_glb_bytes(&world, &excluded).expect("export succeeds");
        let document = gltf::Gltf::from_slice(&bytes).expect("exported GLB parses and validates");
        assert_eq!(document.nodes().count(), 0);
    }
}
