use bytemuck::{Pod, Zeroable};
use glam::{Mat4, Quat, Vec3};
use std::path::Path;

fn compute_aabb(vertices: &[Vertex]) -> (Vec3, Vec3)
{
    let mut aabb_min = Vec3::splat(f32::MAX);
    let mut aabb_max = Vec3::splat(f32::MIN);
    for v in vertices
    {
        let p = Vec3::from(v.position);
        aabb_min = aabb_min.min(p);
        aabb_max = aabb_max.max(p);
    }
    if vertices.is_empty()
    {
        (Vec3::ZERO, Vec3::ZERO)
    }
    else
    {
        (aabb_min, aabb_max)
    }
}

#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub struct Vertex
{
    pub position: [f32; 3],
    pub normal: [f32; 3],
    pub uv: [f32; 2],
}

impl Vertex
{
    pub fn desc() -> wgpu::VertexBufferLayout<'static>
    {
        wgpu::VertexBufferLayout {
            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
            step_mode: wgpu::VertexStepMode::Vertex,
            attributes: &[
                wgpu::VertexAttribute {
                    offset: 0,
                    shader_location: 0,
                    format: wgpu::VertexFormat::Float32x3,
                },
                wgpu::VertexAttribute {
                    offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
                    shader_location: 1,
                    format: wgpu::VertexFormat::Float32x3,
                },
                wgpu::VertexAttribute {
                    offset: (std::mem::size_of::<[f32; 3]>() * 2) as wgpu::BufferAddress,
                    shader_location: 2,
                    format: wgpu::VertexFormat::Float32x2,
                },
            ],
        }
    }
}

#[derive(Debug, Clone)]
pub struct InstanceData
{
    pub position: Vec3,
    pub rotation: Quat,
    pub scale: Vec3,
    pub dissolve_amount: f32,
}

impl InstanceData
{
    pub fn to_raw(&self) -> InstanceRaw
    {
        let model = Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position);
        InstanceRaw {
            model: model.to_cols_array_2d(),
            dissolve_amount: self.dissolve_amount,
            _padding: [0.0; 3],
        }
    }
}

#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub struct InstanceRaw
{
    pub model: [[f32; 4]; 4],
    pub dissolve_amount: f32,
    pub _padding: [f32; 3],
}

impl InstanceRaw
{
    pub fn desc() -> wgpu::VertexBufferLayout<'static>
    {
        wgpu::VertexBufferLayout {
            array_stride: std::mem::size_of::<InstanceRaw>() as wgpu::BufferAddress,
            step_mode: wgpu::VertexStepMode::Instance,
            attributes: &[
                wgpu::VertexAttribute {
                    offset: 0,
                    shader_location: 3,
                    format: wgpu::VertexFormat::Float32x4,
                },
                wgpu::VertexAttribute {
                    offset: std::mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
                    shader_location: 4,
                    format: wgpu::VertexFormat::Float32x4,
                },
                wgpu::VertexAttribute {
                    offset: (std::mem::size_of::<[f32; 4]>() * 2) as wgpu::BufferAddress,
                    shader_location: 5,
                    format: wgpu::VertexFormat::Float32x4,
                },
                wgpu::VertexAttribute {
                    offset: (std::mem::size_of::<[f32; 4]>() * 3) as wgpu::BufferAddress,
                    shader_location: 6,
                    format: wgpu::VertexFormat::Float32x4,
                },
                wgpu::VertexAttribute {
                    offset: (std::mem::size_of::<[f32; 4]>() * 4) as wgpu::BufferAddress,
                    shader_location: 7,
                    format: wgpu::VertexFormat::Float32,
                },
            ],
        }
    }
}

pub struct Mesh
{
    pub vertex_buffer: wgpu::Buffer,
    pub index_buffer: wgpu::Buffer,
    pub num_indices: u32,
    pub aabb_min: Vec3,
    pub aabb_max: Vec3,
    pub cpu_positions: Vec<[f32; 3]>,
    pub cpu_indices: Vec<u32>,
}

impl Mesh
{
    pub fn new(device: &wgpu::Device, vertices: &[Vertex], indices: &[u32]) -> Self
    {
        use wgpu::util::DeviceExt;

        let (aabb_min, aabb_max) = compute_aabb(vertices);

        let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("Vertex Buffer"),
            contents: bytemuck::cast_slice(vertices),
            usage: wgpu::BufferUsages::VERTEX,
        });

        let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("Index Buffer"),
            contents: bytemuck::cast_slice(indices),
            usage: wgpu::BufferUsages::INDEX,
        });

        Self {
            vertex_buffer,
            index_buffer,
            num_indices: indices.len() as u32,
            aabb_min,
            aabb_max,
            cpu_positions: vertices.iter().map(|v| v.position).collect(),
            cpu_indices: indices.to_vec(),
        }
    }

    pub fn create_cube_mesh(device: &wgpu::Device) -> Mesh
    {
        let vertices = vec![
            Vertex {
                position: [-0.5, -0.5, 0.5],
                normal: [0.0, 0.0, 1.0],
                uv: [0.0, 0.0],
            },
            Vertex {
                position: [0.5, -0.5, 0.5],
                normal: [0.0, 0.0, 1.0],
                uv: [1.0, 0.0],
            },
            Vertex {
                position: [0.5, 0.5, 0.5],
                normal: [0.0, 0.0, 1.0],
                uv: [1.0, 1.0],
            },
            Vertex {
                position: [-0.5, 0.5, 0.5],
                normal: [0.0, 0.0, 1.0],
                uv: [0.0, 1.0],
            },
            Vertex {
                position: [0.5, -0.5, 0.5],
                normal: [1.0, 0.0, 0.0],
                uv: [0.0, 0.0],
            },
            Vertex {
                position: [0.5, -0.5, -0.5],
                normal: [1.0, 0.0, 0.0],
                uv: [1.0, 0.0],
            },
            Vertex {
                position: [0.5, 0.5, -0.5],
                normal: [1.0, 0.0, 0.0],
                uv: [1.0, 1.0],
            },
            Vertex {
                position: [0.5, 0.5, 0.5],
                normal: [1.0, 0.0, 0.0],
                uv: [0.0, 1.0],
            },
            Vertex {
                position: [0.5, -0.5, -0.5],
                normal: [0.0, 0.0, -1.0],
                uv: [0.0, 0.0],
            },
            Vertex {
                position: [-0.5, -0.5, -0.5],
                normal: [0.0, 0.0, -1.0],
                uv: [1.0, 0.0],
            },
            Vertex {
                position: [-0.5, 0.5, -0.5],
                normal: [0.0, 0.0, -1.0],
                uv: [1.0, 1.0],
            },
            Vertex {
                position: [0.5, 0.5, -0.5],
                normal: [0.0, 0.0, -1.0],
                uv: [0.0, 1.0],
            },
            Vertex {
                position: [-0.5, -0.5, -0.5],
                normal: [-1.0, 0.0, 0.0],
                uv: [0.0, 0.0],
            },
            Vertex {
                position: [-0.5, -0.5, 0.5],
                normal: [-1.0, 0.0, 0.0],
                uv: [1.0, 0.0],
            },
            Vertex {
                position: [-0.5, 0.5, 0.5],
                normal: [-1.0, 0.0, 0.0],
                uv: [1.0, 1.0],
            },
            Vertex {
                position: [-0.5, 0.5, -0.5],
                normal: [-1.0, 0.0, 0.0],
                uv: [0.0, 1.0],
            },
            Vertex {
                position: [-0.5, 0.5, 0.5],
                normal: [0.0, 1.0, 0.0],
                uv: [0.0, 0.0],
            },
            Vertex {
                position: [0.5, 0.5, 0.5],
                normal: [0.0, 1.0, 0.0],
                uv: [1.0, 0.0],
            },
            Vertex {
                position: [0.5, 0.5, -0.5],
                normal: [0.0, 1.0, 0.0],
                uv: [1.0, 1.0],
            },
            Vertex {
                position: [-0.5, 0.5, -0.5],
                normal: [0.0, 1.0, 0.0],
                uv: [0.0, 1.0],
            },
            Vertex {
                position: [-0.5, -0.5, -0.5],
                normal: [0.0, -1.0, 0.0],
                uv: [0.0, 0.0],
            },
            Vertex {
                position: [0.5, -0.5, -0.5],
                normal: [0.0, -1.0, 0.0],
                uv: [1.0, 0.0],
            },
            Vertex {
                position: [0.5, -0.5, 0.5],
                normal: [0.0, -1.0, 0.0],
                uv: [1.0, 1.0],
            },
            Vertex {
                position: [-0.5, -0.5, 0.5],
                normal: [0.0, -1.0, 0.0],
                uv: [0.0, 1.0],
            },
        ];

        let indices = vec![
            0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4, 8, 9, 10, 10, 11, 8, 12, 13, 14, 14, 15, 12, 16,
            17, 18, 18, 19, 16, 20, 21, 22, 22, 23, 20,
        ];

        Mesh::new(device, &vertices, &indices)
    }

    pub fn create_plane_mesh(
        device: &wgpu::Device,
        width: f32,
        height: f32,
        subdivisions_x: u32,
        subdivisions_y: u32,
    ) -> Mesh
    {
        let mut vertices = Vec::new();
        let mut indices = Vec::new();

        for y in 0..=subdivisions_y
        {
            for x in 0..=subdivisions_x
            {
                let fx = x as f32 / subdivisions_x as f32;
                let fy = y as f32 / subdivisions_y as f32;

                let px = (fx - 0.5) * width;
                let py = 0.0;
                let pz = (fy - 0.5) * height;

                vertices.push(Vertex {
                    position: [px, py, pz],
                    normal: [0.0, 1.0, 0.0],
                    uv: [fx, fy],
                });
            }
        }

        for y in 0..subdivisions_y
        {
            for x in 0..subdivisions_x
            {
                let row_stride = subdivisions_x + 1;
                let i0 = y * row_stride + x;
                let i1 = i0 + 1;
                let i2 = i0 + row_stride;
                let i3 = i2 + 1;

                indices.push(i0);
                indices.push(i2);
                indices.push(i1);

                indices.push(i1);
                indices.push(i2);
                indices.push(i3);
            }
        }

        Mesh::new(device, &vertices, &indices)
    }

    pub fn create_wireframe_box(device: &wgpu::Device) -> Mesh
    {
        let vertices = vec![
            Vertex {
                position: [-0.5, -0.5, -0.5],
                normal: [0.0, 1.0, 0.0],
                uv: [0.0, 0.0],
            },
            Vertex {
                position: [0.5, -0.5, -0.5],
                normal: [0.0, 1.0, 0.0],
                uv: [0.0, 0.0],
            },
            Vertex {
                position: [0.5, -0.5, 0.5],
                normal: [0.0, 1.0, 0.0],
                uv: [0.0, 0.0],
            },
            Vertex {
                position: [-0.5, -0.5, 0.5],
                normal: [0.0, 1.0, 0.0],
                uv: [0.0, 0.0],
            },
            Vertex {
                position: [-0.5, 0.5, -0.5],
                normal: [0.0, 1.0, 0.0],
                uv: [0.0, 0.0],
            },
            Vertex {
                position: [0.5, 0.5, -0.5],
                normal: [0.0, 1.0, 0.0],
                uv: [0.0, 0.0],
            },
            Vertex {
                position: [0.5, 0.5, 0.5],
                normal: [0.0, 1.0, 0.0],
                uv: [0.0, 0.0],
            },
            Vertex {
                position: [-0.5, 0.5, 0.5],
                normal: [0.0, 1.0, 0.0],
                uv: [0.0, 0.0],
            },
        ];

        let indices = vec![
            0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7,
        ];

        Mesh::new(device, &vertices, &indices)
    }

    pub fn load_gltf_mesh(
        device: &wgpu::Device,
        path: impl AsRef<Path>,
    ) -> Result<Mesh, Box<dyn std::error::Error>>
    {
        let (gltf, buffers, _images) = gltf::import(path)?;

        let mut all_vertices = Vec::new();
        let mut all_indices = Vec::new();

        for scene in gltf.scenes()
        {
            for node in scene.nodes()
            {
                Self::process_node(
                    &node,
                    &buffers,
                    Mat4::IDENTITY,
                    &mut all_vertices,
                    &mut all_indices,
                )?;
            }
        }

        Ok(Mesh::new(device, &all_vertices, &all_indices))
    }

    fn process_node(
        node: &gltf::Node,
        buffers: &[gltf::buffer::Data],
        parent_transform: Mat4,
        all_vertices: &mut Vec<Vertex>,
        all_indices: &mut Vec<u32>,
    ) -> Result<(), Box<dyn std::error::Error>>
    {
        let local_transform = Mat4::from_cols_array_2d(&node.transform().matrix());
        let global_transform = parent_transform * local_transform;

        if let Some(mesh) = node.mesh()
        {
            for primitive in mesh.primitives()
            {
                let reader =
                    primitive.reader(|buffer| buffers.get(buffer.index()).map(|data| &data[..]));

                let positions = reader
                    .read_positions()
                    .ok_or("Missing position data")?
                    .collect::<Vec<[f32; 3]>>();

                let normals = reader
                    .read_normals()
                    .ok_or("Missing normal data")?
                    .collect::<Vec<[f32; 3]>>();

                let uvs = reader
                    .read_tex_coords(0)
                    .map(|iter| iter.into_f32().collect::<Vec<[f32; 2]>>())
                    .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);

                let base_index = all_vertices.len() as u32;

                let normal_matrix = global_transform.inverse().transpose();

                for ((pos, normal), uv) in positions.iter().zip(normals.iter()).zip(uvs.iter())
                {
                    let pos_vec3 = Vec3::from(*pos);
                    let normal_vec3 = Vec3::from(*normal);

                    let transformed_pos = global_transform.transform_point3(pos_vec3);
                    let transformed_normal =
                        normal_matrix.transform_vector3(normal_vec3).normalize();

                    all_vertices.push(Vertex {
                        position: transformed_pos.into(),
                        normal: transformed_normal.into(),
                        uv: *uv,
                    });
                }

                if let Some(indices_reader) = reader.read_indices()
                {
                    all_indices.extend(indices_reader.into_u32().map(|i| i + base_index));
                }
            }
        }

        for child in node.children()
        {
            Self::process_node(&child, buffers, global_transform, all_vertices, all_indices)?;
        }

        Ok(())
    }

    pub fn load_mesh(path: impl AsRef<Path>) -> Result<Mesh, Box<dyn std::error::Error>>
    {
        crate::render::with_device(|device| Mesh::load_gltf_mesh(device, path))
    }

    pub fn load_gltf_with_instances(
        device: &wgpu::Device,
        path: impl AsRef<Path>,
    ) -> anyhow::Result<Vec<(Mesh, Vec<InstanceData>)>>
    {
        let path = path.as_ref();
        let gltf_str = std::fs::read_to_string(path)?;
        let gltf_json: serde_json::Value = serde_json::from_str(&gltf_str)?;

        let (document, buffers, _images) = gltf::import(path)?;

        let mut result = Vec::new();

        let nodes = gltf_json["nodes"]
            .as_array()
            .ok_or_else(|| anyhow::anyhow!("Missing nodes array"))?;

        for (node_index, json_node) in nodes.iter().enumerate()
        {
            let node = document
                .nodes()
                .nth(node_index)
                .ok_or_else(|| anyhow::anyhow!("Node index mismatch"))?;

            if let Some(mesh_data) = node.mesh()
            {
                let has_instancing = json_node
                    .get("extensions")
                    .and_then(|ext| ext.get("EXT_mesh_gpu_instancing"))
                    .is_some();

                if has_instancing
                {
                    let extensions = json_node.get("extensions").unwrap();
                    let instancing_ext = extensions.get("EXT_mesh_gpu_instancing").unwrap();
                    let mut mesh_vertices = Vec::new();
                    let mut mesh_indices = Vec::new();

                    for primitive in mesh_data.primitives()
                    {
                        let reader = primitive
                            .reader(|buffer| buffers.get(buffer.index()).map(|data| &data[..]));

                        let positions = reader
                            .read_positions()
                            .ok_or_else(|| anyhow::anyhow!("Missing position data"))?
                            .collect::<Vec<[f32; 3]>>();

                        let normals = reader
                            .read_normals()
                            .ok_or_else(|| anyhow::anyhow!("Missing normal data"))?
                            .collect::<Vec<[f32; 3]>>();

                        let uvs = reader
                            .read_tex_coords(0)
                            .map(|iter| iter.into_f32().collect::<Vec<[f32; 2]>>())
                            .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);

                        let base_index = mesh_vertices.len() as u32;

                        for ((pos, normal), uv) in
                            positions.iter().zip(normals.iter()).zip(uvs.iter())
                        {
                            mesh_vertices.push(Vertex {
                                position: *pos,
                                normal: *normal,
                                uv: *uv,
                            });
                        }

                        if let Some(indices_reader) = reader.read_indices()
                        {
                            mesh_indices.extend(indices_reader.into_u32().map(|i| i + base_index));
                        }
                    }

                    let attributes = instancing_ext
                        .get("attributes")
                        .and_then(|v| v.as_object())
                        .ok_or_else(|| {
                            anyhow::anyhow!("Missing attributes in EXT_mesh_gpu_instancing")
                        })?;

                    let translation_accessor_index = attributes
                        .get("TRANSLATION")
                        .and_then(|v| v.as_u64())
                        .ok_or_else(|| {
                            anyhow::anyhow!("Missing TRANSLATION in instancing extension")
                        })? as usize;

                    let rotation_accessor_index = attributes
                        .get("ROTATION")
                        .and_then(|v| v.as_u64())
                        .ok_or_else(|| {
                            anyhow::anyhow!("Missing ROTATION in instancing extension")
                        })? as usize;

                    let scale_accessor_index = attributes
                        .get("SCALE")
                        .and_then(|v| v.as_u64())
                        .ok_or_else(|| anyhow::anyhow!("Missing SCALE in instancing extension"))?
                        as usize;

                    let translations =
                        Self::read_vec3_accessor(&document, &buffers, translation_accessor_index)?;
                    let rotations =
                        Self::read_quat_accessor(&document, &buffers, rotation_accessor_index)?;
                    let scales =
                        Self::read_vec3_accessor(&document, &buffers, scale_accessor_index)?;

                    let instances: Vec<InstanceData> = translations
                        .into_iter()
                        .zip(rotations.into_iter())
                        .zip(scales.into_iter())
                        .map(|((position, rotation), scale)| InstanceData {
                            position,
                            rotation,
                            scale,
                            dissolve_amount: 0.0,
                        })
                        .collect();

                    let mesh = Mesh::new(device, &mesh_vertices, &mesh_indices);
                    result.push((mesh, instances));
                }
                else
                {
                    let mut mesh_vertices = Vec::new();
                    let mut mesh_indices = Vec::new();

                    for primitive in mesh_data.primitives()
                    {
                        let reader = primitive
                            .reader(|buffer| buffers.get(buffer.index()).map(|data| &data[..]));

                        let positions = reader
                            .read_positions()
                            .ok_or_else(|| anyhow::anyhow!("Missing position data"))?
                            .collect::<Vec<[f32; 3]>>();

                        let normals = reader
                            .read_normals()
                            .ok_or_else(|| anyhow::anyhow!("Missing normal data"))?
                            .collect::<Vec<[f32; 3]>>();

                        let uvs = reader
                            .read_tex_coords(0)
                            .map(|iter| iter.into_f32().collect::<Vec<[f32; 2]>>())
                            .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);

                        let base_index = mesh_vertices.len() as u32;

                        for ((pos, normal), uv) in
                            positions.iter().zip(normals.iter()).zip(uvs.iter())
                        {
                            mesh_vertices.push(Vertex {
                                position: *pos,
                                normal: *normal,
                                uv: *uv,
                            });
                        }

                        if let Some(indices_reader) = reader.read_indices()
                        {
                            mesh_indices.extend(indices_reader.into_u32().map(|i| i + base_index));
                        }
                    }

                    let mesh = Mesh::new(device, &mesh_vertices, &mesh_indices);
                    result.push((mesh, Vec::new()));
                }
            }
        }

        Ok(result)
    }

    fn read_vec3_accessor(
        document: &gltf::Document,
        buffers: &[gltf::buffer::Data],
        accessor_index: usize,
    ) -> anyhow::Result<Vec<Vec3>>
    {
        let accessor = document
            .accessors()
            .nth(accessor_index)
            .ok_or_else(|| anyhow::anyhow!("Invalid accessor index"))?;

        let buffer_view = accessor
            .view()
            .ok_or_else(|| anyhow::anyhow!("Missing buffer view"))?;
        let buffer = &buffers[buffer_view.buffer().index()];
        let start = buffer_view.offset() + accessor.offset();
        let stride = buffer_view.stride().unwrap_or(12);

        let mut result = Vec::new();
        for i in 0..accessor.count()
        {
            let offset = start + i * stride;
            let x = f32::from_le_bytes([
                buffer[offset],
                buffer[offset + 1],
                buffer[offset + 2],
                buffer[offset + 3],
            ]);
            let y = f32::from_le_bytes([
                buffer[offset + 4],
                buffer[offset + 5],
                buffer[offset + 6],
                buffer[offset + 7],
            ]);
            let z = f32::from_le_bytes([
                buffer[offset + 8],
                buffer[offset + 9],
                buffer[offset + 10],
                buffer[offset + 11],
            ]);
            result.push(Vec3::new(x, y, z));
        }

        Ok(result)
    }

    fn read_quat_accessor(
        document: &gltf::Document,
        buffers: &[gltf::buffer::Data],
        accessor_index: usize,
    ) -> anyhow::Result<Vec<Quat>>
    {
        let accessor = document
            .accessors()
            .nth(accessor_index)
            .ok_or_else(|| anyhow::anyhow!("Invalid accessor index"))?;

        let buffer_view = accessor
            .view()
            .ok_or_else(|| anyhow::anyhow!("Missing buffer view"))?;
        let buffer = &buffers[buffer_view.buffer().index()];
        let start = buffer_view.offset() + accessor.offset();
        let stride = buffer_view.stride().unwrap_or(16);

        let mut result = Vec::new();
        for i in 0..accessor.count()
        {
            let offset = start + i * stride;
            let x = f32::from_le_bytes([
                buffer[offset],
                buffer[offset + 1],
                buffer[offset + 2],
                buffer[offset + 3],
            ]);
            let y = f32::from_le_bytes([
                buffer[offset + 4],
                buffer[offset + 5],
                buffer[offset + 6],
                buffer[offset + 7],
            ]);
            let z = f32::from_le_bytes([
                buffer[offset + 8],
                buffer[offset + 9],
                buffer[offset + 10],
                buffer[offset + 11],
            ]);
            let w = f32::from_le_bytes([
                buffer[offset + 12],
                buffer[offset + 13],
                buffer[offset + 14],
                buffer[offset + 15],
            ]);
            result.push(Quat::from_xyzw(x, y, z, w));
        }

        Ok(result)
    }
}
