use crate::{
    model::{self, AlphaMode},
    texture::{self, TextureColorSpace, TextureSamplerDescriptor},
};
use anyhow::{Context, bail};
use eframe::wgpu;
use eframe::wgpu::util::DeviceExt;
use glam::{Mat3, Mat4, Vec2, Vec3};
use std::path::{Path, PathBuf};

const WHITE_TEXTURE: &[u8; 4] = &[255, 255, 255, 255];
const BLACK_TEXTURE: &[u8; 4] = &[0, 0, 0, 255];
const FLAT_NORMAL_TEXTURE: &[u8; 4] = &[128, 128, 255, 255];

#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct MaterialParams {
    base_color: [f32; 4],
    metallic_roughness: [f32; 4],
    emissive_alpha: [f32; 4],
    flags: [f32; 4],
}

fn resource_path(file_name: &str) -> PathBuf {
    Path::new(env!("RESOURCE_DIR")).join(file_name)
}

pub fn load_string(file_name: &str) -> anyhow::Result<String> {
    Ok(std::fs::read_to_string(resource_path(file_name))?)
}

pub fn load_binary(file_name: &str) -> anyhow::Result<Vec<u8>> {
    Ok(std::fs::read(resource_path(file_name))?)
}

pub fn load_texture(
    file_name: &str,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
) -> anyhow::Result<texture::Texture> {
    // let data = load_binary(file_name)?;
    let data = include_bytes!("../res/hand.png");
    texture::Texture::from_bytes(device, queue, data, file_name)
}

pub fn load_model(
    file_name: &str,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    texture_bind_group_layout: &wgpu::BindGroupLayout,
) -> anyhow::Result<model::Model> {
    match Path::new(file_name)
        .extension()
        .and_then(|extension| extension.to_str())
        .map(str::to_ascii_lowercase)
        .as_deref()
    {
        Some("obj") => load_obj_model(file_name, device, queue, texture_bind_group_layout),
        Some("gltf") | Some("glb") => {
            load_gltf_model(file_name, device, queue, texture_bind_group_layout)
        }
        _ => bail!("unsupported model format: {file_name}"),
    }
}

fn load_obj_model(
    file_name: &str,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    texture_bind_group_layout: &wgpu::BindGroupLayout,
) -> anyhow::Result<model::Model> {
    let path = resource_path(file_name);
    let (models, obj_materials) = tobj::load_obj(
        &path,
        &tobj::LoadOptions {
            single_index: true,
            triangulate: true,
            ..Default::default()
        },
    )?;

    let mut materials = Vec::new();
    for material in obj_materials? {
        let base_color = material
            .diffuse
            .map(|diffuse| [diffuse[0], diffuse[1], diffuse[2], 1.0])
            .unwrap_or([1.0, 1.0, 1.0, 1.0]);
        let base_color_texture = match material.diffuse_texture.as_deref() {
            Some(texture_path) => load_texture(texture_path, device, queue)?,
            None => default_texture(
                device,
                queue,
                &material.name,
                TextureColorSpace::Srgb,
                WHITE_TEXTURE,
            )?,
        };

        let material_textures = MaterialTextures {
            base_color_texture,
            metallic_roughness_texture: default_texture(
                device,
                queue,
                &format!("{} metallic-roughness", material.name),
                TextureColorSpace::Linear,
                WHITE_TEXTURE,
            )?,
            normal_texture: default_texture(
                device,
                queue,
                &format!("{} normal", material.name),
                TextureColorSpace::Linear,
                FLAT_NORMAL_TEXTURE,
            )?,
            occlusion_texture: default_texture(
                device,
                queue,
                &format!("{} occlusion", material.name),
                TextureColorSpace::Linear,
                WHITE_TEXTURE,
            )?,
            emissive_texture: default_texture(
                device,
                queue,
                &format!("{} emissive", material.name),
                TextureColorSpace::Srgb,
                BLACK_TEXTURE,
            )?,
        };
        let params = MaterialParams {
            base_color,
            metallic_roughness: [0.0, 0.5, 1.0, 1.0],
            emissive_alpha: [0.0, 0.0, 0.0, 0.0],
            flags: [0.5, 0.0, 0.0, 0.0],
        };
        log_material_debug(
            materials.len(),
            &material.name,
            AlphaMode::Opaque,
            0.5,
            base_color[3],
            false,
        );
        materials.push(create_material(
            device,
            texture_bind_group_layout,
            material.name,
            material_textures,
            params,
            AlphaMode::Opaque,
            0.5,
            base_color,
            false,
        ));
    }

    if materials.is_empty() {
        materials.push(create_color_material(
            "default",
            [1.0, 1.0, 1.0, 1.0],
            device,
            queue,
            texture_bind_group_layout,
        )?);
    }

    let meshes = models
        .into_iter()
        .map(|m| {
            let mut vertices = (0..m.mesh.positions.len() / 3)
                .map(|i| model::ModelVertex {
                    position: [
                        m.mesh.positions[i * 3],
                        m.mesh.positions[i * 3 + 1],
                        m.mesh.positions[i * 3 + 2],
                    ],
                    tex_coords: if m.mesh.texcoords.len() >= i * 2 + 2 {
                        [m.mesh.texcoords[i * 2], 1.0 - m.mesh.texcoords[i * 2 + 1]]
                    } else {
                        [0.0, 0.0]
                    },
                    normal: if m.mesh.normals.len() >= i * 3 + 3 {
                        [
                            m.mesh.normals[i * 3],
                            m.mesh.normals[i * 3 + 1],
                            m.mesh.normals[i * 3 + 2],
                        ]
                    } else {
                        [0.0, 1.0, 0.0]
                    },
                    color: [1.0, 1.0, 1.0, 1.0],
                    tangent: [1.0, 0.0, 0.0, 1.0],
                })
                .collect::<Vec<_>>();
            generate_tangents(&mut vertices, &m.mesh.indices);

            let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some(&format!("{} Vertex Buffer", m.name)),
                contents: bytemuck::cast_slice(&vertices),
                usage: wgpu::BufferUsages::VERTEX,
            });
            let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some(&format!("{} Index Buffer", m.name)),
                contents: bytemuck::cast_slice(&m.mesh.indices),
                usage: wgpu::BufferUsages::INDEX,
            });

            model::Mesh {
                name: m.name,
                vertex_buffer,
                index_buffer,
                num_elements: m.mesh.indices.len() as u32,
                material: m.mesh.material_id.unwrap_or(0),
                center: mesh_center(&vertices),
            }
        })
        .collect::<Vec<_>>();

    Ok(model::Model { meshes, materials })
}

fn load_gltf_model(
    file_name: &str,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    texture_bind_group_layout: &wgpu::BindGroupLayout,
) -> anyhow::Result<model::Model> {
    let path = resource_path(file_name);
    let (document, buffers, images) =
        gltf::import(&path).with_context(|| format!("failed to import {file_name}"))?;

    let mut materials = Vec::new();
    for material in document.materials() {
        materials.push(create_gltf_material(
            &material,
            &images,
            device,
            queue,
            texture_bind_group_layout,
        )?);
    }

    let default_material = materials.len();
    materials.push(create_color_material(
        "default",
        [1.0, 1.0, 1.0, 1.0],
        device,
        queue,
        texture_bind_group_layout,
    )?);

    let mut meshes = Vec::new();
    let scene = document
        .default_scene()
        .or_else(|| document.scenes().next())
        .context("glTF has no scene")?;
    for node in scene.nodes() {
        append_gltf_node_meshes(
            &node,
            Mat4::IDENTITY,
            &buffers,
            device,
            &mut meshes,
            default_material,
        )?;
    }

    Ok(model::Model { meshes, materials })
}

struct MaterialTextures {
    base_color_texture: texture::Texture,
    metallic_roughness_texture: texture::Texture,
    normal_texture: texture::Texture,
    occlusion_texture: texture::Texture,
    emissive_texture: texture::Texture,
}

fn create_gltf_material(
    material: &gltf::Material<'_>,
    images: &[gltf::image::Data],
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    texture_bind_group_layout: &wgpu::BindGroupLayout,
) -> anyhow::Result<model::Material> {
    let pbr = material.pbr_metallic_roughness();
    let name = material.name().unwrap_or("gltf_material");
    let normal_info = material.normal_texture();
    let occlusion_info = material.occlusion_texture();

    let material_textures = MaterialTextures {
        base_color_texture: texture_from_gltf_texture(
            pbr.base_color_texture().map(|info| info.texture()),
            images,
            device,
            queue,
            &format!("{name} baseColor"),
            TextureColorSpace::Srgb,
            WHITE_TEXTURE,
        )?,
        metallic_roughness_texture: texture_from_gltf_texture(
            pbr.metallic_roughness_texture().map(|info| info.texture()),
            images,
            device,
            queue,
            &format!("{name} metallicRoughness"),
            TextureColorSpace::Linear,
            WHITE_TEXTURE,
        )?,
        normal_texture: texture_from_gltf_texture(
            normal_info.as_ref().map(|info| info.texture()),
            images,
            device,
            queue,
            &format!("{name} normal"),
            TextureColorSpace::Linear,
            FLAT_NORMAL_TEXTURE,
        )?,
        occlusion_texture: texture_from_gltf_texture(
            occlusion_info.as_ref().map(|info| info.texture()),
            images,
            device,
            queue,
            &format!("{name} occlusion"),
            TextureColorSpace::Linear,
            WHITE_TEXTURE,
        )?,
        emissive_texture: texture_from_gltf_texture(
            material.emissive_texture().map(|info| info.texture()),
            images,
            device,
            queue,
            &format!("{name} emissive"),
            TextureColorSpace::Srgb,
            BLACK_TEXTURE,
        )?,
    };

    let alpha_mode = match material.alpha_mode() {
        gltf::material::AlphaMode::Opaque => AlphaMode::Opaque,
        gltf::material::AlphaMode::Mask => AlphaMode::Mask,
        gltf::material::AlphaMode::Blend => AlphaMode::Blend,
    };
    let normal_scale = normal_info.as_ref().map(|info| info.scale()).unwrap_or(1.0);
    let occlusion_strength = occlusion_info
        .as_ref()
        .map(|info| info.strength())
        .unwrap_or(1.0);
    let emissive_factor = material.emissive_factor();
    let base_color_factor = pbr.base_color_factor();
    let alpha_cutoff = material.alpha_cutoff().unwrap_or(0.5);
    let double_sided = material.double_sided();
    let params = MaterialParams {
        base_color: base_color_factor,
        metallic_roughness: [
            pbr.metallic_factor(),
            pbr.roughness_factor(),
            normal_scale,
            occlusion_strength,
        ],
        emissive_alpha: [
            emissive_factor[0],
            emissive_factor[1],
            emissive_factor[2],
            alpha_mode.shader_value(),
        ],
        flags: [
            alpha_cutoff,
            0.0,
            if normal_info.is_some() { 1.0 } else { 0.0 },
            if double_sided { 1.0 } else { 0.0 },
        ],
    };
    log_material_debug(
        material.index().unwrap_or(usize::MAX),
        name,
        alpha_mode,
        alpha_cutoff,
        base_color_factor[3],
        double_sided,
    );

    Ok(create_material(
        device,
        texture_bind_group_layout,
        name.to_owned(),
        material_textures,
        params,
        alpha_mode,
        alpha_cutoff,
        base_color_factor,
        double_sided,
    ))
}

fn create_color_material(
    name: &str,
    color: [f32; 4],
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    texture_bind_group_layout: &wgpu::BindGroupLayout,
) -> anyhow::Result<model::Material> {
    let material_textures = MaterialTextures {
        base_color_texture: default_texture(
            device,
            queue,
            name,
            TextureColorSpace::Srgb,
            WHITE_TEXTURE,
        )?,
        metallic_roughness_texture: default_texture(
            device,
            queue,
            &format!("{name} metallic-roughness"),
            TextureColorSpace::Linear,
            WHITE_TEXTURE,
        )?,
        normal_texture: default_texture(
            device,
            queue,
            &format!("{name} normal"),
            TextureColorSpace::Linear,
            FLAT_NORMAL_TEXTURE,
        )?,
        occlusion_texture: default_texture(
            device,
            queue,
            &format!("{name} occlusion"),
            TextureColorSpace::Linear,
            WHITE_TEXTURE,
        )?,
        emissive_texture: default_texture(
            device,
            queue,
            &format!("{name} emissive"),
            TextureColorSpace::Srgb,
            BLACK_TEXTURE,
        )?,
    };
    let params = MaterialParams {
        base_color: color,
        metallic_roughness: [1.0, 1.0, 1.0, 1.0],
        emissive_alpha: [0.0, 0.0, 0.0, 0.0],
        flags: [0.5, 0.0, 0.0, 0.0],
    };
    log_material_debug(usize::MAX, name, AlphaMode::Opaque, 0.5, color[3], false);

    Ok(create_material(
        device,
        texture_bind_group_layout,
        name.to_owned(),
        material_textures,
        params,
        AlphaMode::Opaque,
        0.5,
        color,
        false,
    ))
}

fn create_material(
    device: &wgpu::Device,
    texture_bind_group_layout: &wgpu::BindGroupLayout,
    name: String,
    textures: MaterialTextures,
    params: MaterialParams,
    alpha_mode: AlphaMode,
    alpha_cutoff: f32,
    base_color_factor: [f32; 4],
    double_sided: bool,
) -> model::Material {
    let params_buffer = create_material_params_buffer(device, &name, params);
    let bind_group = create_texture_bind_group(
        device,
        texture_bind_group_layout,
        &name,
        &textures,
        &params_buffer,
    );

    model::Material {
        name,
        alpha_mode,
        alpha_cutoff,
        base_color_factor,
        double_sided,
        base_color_texture: textures.base_color_texture,
        metallic_roughness_texture: textures.metallic_roughness_texture,
        normal_texture: textures.normal_texture,
        occlusion_texture: textures.occlusion_texture,
        emissive_texture: textures.emissive_texture,
        params_buffer,
        bind_group,
    }
}

fn default_texture(
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    label: &str,
    color_space: TextureColorSpace,
    rgba: &[u8; 4],
) -> anyhow::Result<texture::Texture> {
    texture::Texture::from_rgba8_with_sampler(
        device,
        queue,
        rgba,
        1,
        1,
        label,
        color_space,
        TextureSamplerDescriptor::default(),
    )
}

fn create_material_params_buffer(
    device: &wgpu::Device,
    name: &str,
    params: MaterialParams,
) -> wgpu::Buffer {
    device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
        label: Some(&format!("{name} Material Params Buffer")),
        contents: bytemuck::cast_slice(&[params]),
        usage: wgpu::BufferUsages::UNIFORM,
    })
}

fn create_texture_bind_group(
    device: &wgpu::Device,
    texture_bind_group_layout: &wgpu::BindGroupLayout,
    name: &str,
    textures: &MaterialTextures,
    params_buffer: &wgpu::Buffer,
) -> wgpu::BindGroup {
    device.create_bind_group(&wgpu::BindGroupDescriptor {
        label: Some(&format!("{name} Texture Bind Group")),
        layout: texture_bind_group_layout,
        entries: &[
            wgpu::BindGroupEntry {
                binding: 0,
                resource: wgpu::BindingResource::TextureView(&textures.base_color_texture.view),
            },
            wgpu::BindGroupEntry {
                binding: 1,
                resource: wgpu::BindingResource::Sampler(&textures.base_color_texture.sampler),
            },
            wgpu::BindGroupEntry {
                binding: 2,
                resource: params_buffer.as_entire_binding(),
            },
            wgpu::BindGroupEntry {
                binding: 3,
                resource: wgpu::BindingResource::TextureView(
                    &textures.metallic_roughness_texture.view,
                ),
            },
            wgpu::BindGroupEntry {
                binding: 4,
                resource: wgpu::BindingResource::Sampler(
                    &textures.metallic_roughness_texture.sampler,
                ),
            },
            wgpu::BindGroupEntry {
                binding: 5,
                resource: wgpu::BindingResource::TextureView(&textures.normal_texture.view),
            },
            wgpu::BindGroupEntry {
                binding: 6,
                resource: wgpu::BindingResource::Sampler(&textures.normal_texture.sampler),
            },
            wgpu::BindGroupEntry {
                binding: 7,
                resource: wgpu::BindingResource::TextureView(&textures.occlusion_texture.view),
            },
            wgpu::BindGroupEntry {
                binding: 8,
                resource: wgpu::BindingResource::Sampler(&textures.occlusion_texture.sampler),
            },
            wgpu::BindGroupEntry {
                binding: 9,
                resource: wgpu::BindingResource::TextureView(&textures.emissive_texture.view),
            },
            wgpu::BindGroupEntry {
                binding: 10,
                resource: wgpu::BindingResource::Sampler(&textures.emissive_texture.sampler),
            },
        ],
    })
}

fn texture_from_gltf_texture(
    gltf_texture: Option<gltf::Texture<'_>>,
    images: &[gltf::image::Data],
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    label: &str,
    color_space: TextureColorSpace,
    fallback_rgba: &[u8; 4],
) -> anyhow::Result<texture::Texture> {
    let Some(gltf_texture) = gltf_texture else {
        return default_texture(device, queue, label, color_space, fallback_rgba);
    };

    let image_index = gltf_texture.source().index();
    let image = images
        .get(image_index)
        .with_context(|| format!("glTF texture references missing image {image_index}"))?;
    let rgba = gltf_image_to_rgba8(image)?;
    let sampler = gltf_sampler_descriptor(&gltf_texture.sampler());
    texture::Texture::from_rgba8_with_sampler(
        device,
        queue,
        &rgba,
        image.width,
        image.height,
        &format!("{label} image {image_index} {color_space:?}"),
        color_space,
        sampler,
    )
}

fn gltf_sampler_descriptor(sampler: &gltf::texture::Sampler<'_>) -> TextureSamplerDescriptor {
    use gltf::texture::{MagFilter, MinFilter};

    let mag_filter = match sampler.mag_filter().unwrap_or(MagFilter::Linear) {
        MagFilter::Nearest => wgpu::FilterMode::Nearest,
        MagFilter::Linear => wgpu::FilterMode::Linear,
    };
    let (min_filter, mipmap_filter) = match sampler.min_filter().unwrap_or(MinFilter::Linear) {
        MinFilter::Nearest | MinFilter::NearestMipmapNearest | MinFilter::NearestMipmapLinear => {
            (wgpu::FilterMode::Nearest, wgpu::MipmapFilterMode::Nearest)
        }
        MinFilter::Linear | MinFilter::LinearMipmapNearest => {
            (wgpu::FilterMode::Linear, wgpu::MipmapFilterMode::Nearest)
        }
        MinFilter::LinearMipmapLinear => (wgpu::FilterMode::Linear, wgpu::MipmapFilterMode::Linear),
    };

    TextureSamplerDescriptor {
        address_mode_u: gltf_wrap_mode(sampler.wrap_s()),
        address_mode_v: gltf_wrap_mode(sampler.wrap_t()),
        mag_filter,
        min_filter,
        mipmap_filter,
    }
}

fn gltf_wrap_mode(mode: gltf::texture::WrappingMode) -> wgpu::AddressMode {
    use gltf::texture::WrappingMode;

    match mode {
        WrappingMode::ClampToEdge => wgpu::AddressMode::ClampToEdge,
        WrappingMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat,
        WrappingMode::Repeat => wgpu::AddressMode::Repeat,
    }
}

fn append_gltf_node_meshes(
    node: &gltf::Node<'_>,
    parent_transform: Mat4,
    buffers: &[gltf::buffer::Data],
    device: &wgpu::Device,
    meshes: &mut Vec<model::Mesh>,
    default_material: usize,
) -> anyhow::Result<()> {
    let local_transform = Mat4::from_cols_array_2d(&node.transform().matrix());
    let world_transform = parent_transform * local_transform;

    if let Some(gltf_mesh) = node.mesh() {
        let node_name = node.name().unwrap_or("gltf_node");
        let mesh_index = gltf_mesh.index();
        let mesh_name = node
            .name()
            .or_else(|| gltf_mesh.name())
            .unwrap_or("gltf_mesh");
        for (primitive_index, primitive) in gltf_mesh.primitives().enumerate() {
            append_gltf_primitive_mesh(
                mesh_name,
                primitive_index,
                &primitive,
                world_transform,
                buffers,
                device,
                meshes,
                default_material,
                node_name,
                mesh_index,
            )?;
        }
    }

    for child in node.children() {
        append_gltf_node_meshes(
            &child,
            world_transform,
            buffers,
            device,
            meshes,
            default_material,
        )?;
    }

    Ok(())
}

fn append_gltf_primitive_mesh(
    mesh_name: &str,
    primitive_index: usize,
    primitive: &gltf::Primitive<'_>,
    world_transform: Mat4,
    buffers: &[gltf::buffer::Data],
    device: &wgpu::Device,
    meshes: &mut Vec<model::Mesh>,
    default_material: usize,
    node_name: &str,
    mesh_index: usize,
) -> anyhow::Result<()> {
    let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()]));
    let positions = reader
        .read_positions()
        .with_context(|| format!("{mesh_name} primitive {primitive_index} has no positions"))?
        .collect::<Vec<_>>();
    let normals = reader
        .read_normals()
        .map(|iter| iter.collect::<Vec<_>>())
        .unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; positions.len()]);
    let tex_coords = reader
        .read_tex_coords(0)
        .map(|coords| coords.into_f32().collect::<Vec<_>>())
        .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
    let colors = reader
        .read_colors(0)
        .map(|colors| colors.into_rgba_f32().collect::<Vec<_>>())
        .unwrap_or_else(|| vec![[1.0, 1.0, 1.0, 1.0]; positions.len()]);
    let tangents = reader.read_tangents().map(|iter| iter.collect::<Vec<_>>());
    let has_tangents = tangents.is_some();
    let tangents = tangents.unwrap_or_else(|| vec![[1.0, 0.0, 0.0, 1.0]; positions.len()]);

    let normal_transform = Mat3::from_mat4(world_transform.inverse().transpose());
    let mut vertices = positions
        .iter()
        .enumerate()
        .map(|(index, position)| {
            let world_position = world_transform
                .transform_point3(Vec3::from_array(*position))
                .to_array();
            let world_normal = normal_transform
                .mul_vec3(Vec3::from_array(
                    normals.get(index).copied().unwrap_or([0.0, 1.0, 0.0]),
                ))
                .normalize_or_zero()
                .to_array();
            let tangent = tangents.get(index).copied().unwrap_or([1.0, 0.0, 0.0, 1.0]);
            let world_tangent = normal_transform
                .mul_vec3(Vec3::new(tangent[0], tangent[1], tangent[2]))
                .normalize_or_zero();

            model::ModelVertex {
                position: world_position,
                tex_coords: tex_coords.get(index).copied().unwrap_or([0.0, 0.0]),
                normal: world_normal,
                color: colors.get(index).copied().unwrap_or([1.0, 1.0, 1.0, 1.0]),
                tangent: [
                    world_tangent.x,
                    world_tangent.y,
                    world_tangent.z,
                    tangent[3],
                ],
            }
        })
        .collect::<Vec<_>>();

    let indices = reader
        .read_indices()
        .map(|indices| indices.into_u32().collect::<Vec<_>>())
        .unwrap_or_else(|| (0..vertices.len() as u32).collect());
    let center = mesh_center(&vertices);

    if !has_tangents {
        generate_tangents(&mut vertices, &indices);
    }

    log_primitive_material_debug(
        node_name,
        mesh_index,
        primitive_index,
        &primitive.material(),
        default_material,
    );

    let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
        label: Some(&format!(
            "{mesh_name} Primitive {primitive_index} Vertex Buffer"
        )),
        contents: bytemuck::cast_slice(&vertices),
        usage: wgpu::BufferUsages::VERTEX,
    });
    let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
        label: Some(&format!(
            "{mesh_name} Primitive {primitive_index} Index Buffer"
        )),
        contents: bytemuck::cast_slice(&indices),
        usage: wgpu::BufferUsages::INDEX,
    });

    meshes.push(model::Mesh {
        name: format!("{mesh_name}_{primitive_index}"),
        vertex_buffer,
        index_buffer,
        num_elements: indices.len() as u32,
        material: primitive.material().index().unwrap_or(default_material),
        center,
    });

    Ok(())
}

fn mesh_center(vertices: &[model::ModelVertex]) -> [f32; 3] {
    if vertices.is_empty() {
        return [0.0, 0.0, 0.0];
    }

    let sum = vertices.iter().fold(Vec3::ZERO, |sum, vertex| {
        sum + Vec3::from_array(vertex.position)
    });
    (sum / vertices.len() as f32).to_array()
}

fn log_material_debug(
    index: usize,
    name: &str,
    alpha_mode: AlphaMode,
    alpha_cutoff: f32,
    base_color_factor_alpha: f32,
    double_sided: bool,
) {
    log::warn!(
        "material index={index} name=\"{name}\" alpha_mode={} alpha_cutoff={alpha_cutoff:.3} base_color_factor_alpha={base_color_factor_alpha:.3} double_sided={double_sided} selected_pipeline={}",
        alpha_mode.as_str(),
        selected_pipeline_label(alpha_mode, double_sided),
    );
}

fn log_primitive_material_debug(
    node_name: &str,
    mesh_index: usize,
    primitive_index: usize,
    material: &gltf::Material<'_>,
    default_material: usize,
) {
    let pbr = material.pbr_metallic_roughness();
    let material_index = material.index().unwrap_or(default_material);
    let material_name = material.name().unwrap_or("default");
    log::warn!(
        "primitive node=\"{node_name}\" mesh_index={mesh_index} primitive_index={primitive_index} material_index={material_index} material_name=\"{material_name}\" baseColorFactor={:?} metallicFactor={:.3} roughnessFactor={:.3} alphaMode={:?}",
        pbr.base_color_factor(),
        pbr.metallic_factor(),
        pbr.roughness_factor(),
        material.alpha_mode(),
    );
}

fn selected_pipeline_label(alpha_mode: AlphaMode, double_sided: bool) -> String {
    format!(
        "{}_{}",
        alpha_mode.as_str(),
        if double_sided {
            "DOUBLE_SIDED"
        } else {
            "SINGLE_SIDED"
        }
    )
}

fn generate_tangents(vertices: &mut [model::ModelVertex], indices: &[u32]) {
    let mut accumulated = vec![Vec3::ZERO; vertices.len()];

    for triangle in indices.chunks_exact(3) {
        let [i0, i1, i2] = [
            triangle[0] as usize,
            triangle[1] as usize,
            triangle[2] as usize,
        ];
        if i0 >= vertices.len() || i1 >= vertices.len() || i2 >= vertices.len() {
            continue;
        }

        let p0 = Vec3::from_array(vertices[i0].position);
        let p1 = Vec3::from_array(vertices[i1].position);
        let p2 = Vec3::from_array(vertices[i2].position);
        let uv0 = Vec2::from_array(vertices[i0].tex_coords);
        let uv1 = Vec2::from_array(vertices[i1].tex_coords);
        let uv2 = Vec2::from_array(vertices[i2].tex_coords);

        let delta_pos1 = p1 - p0;
        let delta_pos2 = p2 - p0;
        let delta_uv1 = uv1 - uv0;
        let delta_uv2 = uv2 - uv0;
        let determinant = delta_uv1.x * delta_uv2.y - delta_uv1.y * delta_uv2.x;
        if determinant.abs() < 1e-6 {
            continue;
        }

        let tangent = (delta_pos1 * delta_uv2.y - delta_pos2 * delta_uv1.y) / determinant;
        accumulated[i0] += tangent;
        accumulated[i1] += tangent;
        accumulated[i2] += tangent;
    }

    for (vertex, tangent) in vertices.iter_mut().zip(accumulated) {
        let normal = Vec3::from_array(vertex.normal).normalize_or_zero();
        let tangent = (tangent - normal * normal.dot(tangent)).normalize_or_zero();
        let tangent = if tangent.length_squared() > 0.0 {
            tangent
        } else {
            fallback_tangent(normal)
        };
        vertex.tangent = [tangent.x, tangent.y, tangent.z, 1.0];
    }
}

fn fallback_tangent(normal: Vec3) -> Vec3 {
    let axis = if normal.y.abs() < 0.9 {
        Vec3::Y
    } else {
        Vec3::X
    };
    normal.cross(axis).normalize_or_zero()
}

fn gltf_image_to_rgba8(image: &gltf::image::Data) -> anyhow::Result<Vec<u8>> {
    use gltf::image::Format;

    match image.format {
        Format::R8G8B8A8 => Ok(image.pixels.clone()),
        Format::R8G8B8 => {
            let mut rgba = Vec::with_capacity((image.width * image.height * 4) as usize);
            for rgb in image.pixels.chunks_exact(3) {
                rgba.extend_from_slice(&[rgb[0], rgb[1], rgb[2], 255]);
            }
            Ok(rgba)
        }
        Format::R8 => {
            let mut rgba = Vec::with_capacity((image.width * image.height * 4) as usize);
            for value in &image.pixels {
                rgba.extend_from_slice(&[*value, *value, *value, 255]);
            }
            Ok(rgba)
        }
        Format::R8G8 => {
            let mut rgba = Vec::with_capacity((image.width * image.height * 4) as usize);
            for rg in image.pixels.chunks_exact(2) {
                rgba.extend_from_slice(&[rg[0], rg[1], 0, 255]);
            }
            Ok(rgba)
        }
        Format::R16 => rgba_from_chunks(&image.pixels, 1, 2, read_u16_component),
        Format::R16G16 => rgba_from_chunks(&image.pixels, 2, 2, read_u16_component),
        Format::R16G16B16 => rgba_from_chunks(&image.pixels, 3, 2, read_u16_component),
        Format::R16G16B16A16 => rgba_from_chunks(&image.pixels, 4, 2, read_u16_component),
        Format::R32G32B32FLOAT => rgba_from_chunks(&image.pixels, 3, 4, read_f32_component),
        Format::R32G32B32A32FLOAT => rgba_from_chunks(&image.pixels, 4, 4, read_f32_component),
    }
}

fn rgba_from_chunks(
    pixels: &[u8],
    channels: usize,
    component_width: usize,
    read_component: fn(&[u8]) -> u8,
) -> anyhow::Result<Vec<u8>> {
    let pixel_width = channels * component_width;
    if pixel_width == 0 || pixels.len() % pixel_width != 0 {
        bail!("invalid glTF image byte length for {channels} channels");
    }

    let mut rgba = Vec::with_capacity((pixels.len() / pixel_width) * 4);
    for pixel in pixels.chunks_exact(pixel_width) {
        let r = read_component(&pixel[0..component_width]);
        let g = if channels > 1 {
            read_component(&pixel[component_width..component_width * 2])
        } else {
            r
        };
        let b = if channels > 2 {
            read_component(&pixel[component_width * 2..component_width * 3])
        } else {
            r
        };
        let a = if channels > 3 {
            read_component(&pixel[component_width * 3..component_width * 4])
        } else {
            255
        };
        rgba.extend_from_slice(&[r, g, b, a]);
    }
    Ok(rgba)
}

fn read_u16_component(bytes: &[u8]) -> u8 {
    let value = u16::from_ne_bytes([bytes[0], bytes[1]]);
    (value / 257) as u8
}

fn read_f32_component(bytes: &[u8]) -> u8 {
    let value = f32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
    (value.clamp(0.0, 1.0) * 255.0).round() as u8
}
