use std::collections::HashSet;

pub(super) use super::super::projection::{LightData, MAX_SPOTLIGHT_SHADOWS};
pub(super) use crate::render::wgpu::passes::shadow_depth::{
    MAX_POINT_LIGHT_SHADOWS, PointLightShadowData,
};

pub(super) const INITIAL_INSTANCES: usize = 1_000;
pub(super) const MAX_INSTANCES: usize = 1_000_000;
pub(super) const INITIAL_VERTEX_BUFFER_SIZE: usize = 10_000;
pub(super) const INITIAL_INDEX_BUFFER_SIZE: usize = 30_000;
pub(super) const INITIAL_MATERIALS: usize = 100;
pub(super) const INITIAL_LIGHTS: usize = 32;
pub(super) const MAX_LIGHTS: usize = 128;
pub(super) const BUFFER_GROWTH_FACTOR: f32 = 2.0;
pub(super) const BUFFER_SHRINK_THRESHOLD: f32 = 0.25;
pub(super) const BUFFER_COMPACTION_INTERVAL_FRAMES: u64 = 600;

#[repr(C, align(256))]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct MeshUniforms {
    pub(super) view: [[f32; 4]; 4],
    pub(super) projection: [[f32; 4]; 4],
    pub(super) camera_position: [f32; 4],
    pub(super) num_lights: [u32; 4],
    pub(super) ambient_light: [f32; 4],
    pub(super) light_view_projection: [[f32; 4]; 4],
    pub(super) shadow_bias: f32,
    pub(super) shadows_enabled: f32,
    pub(super) global_unlit: f32,
    pub(super) shadow_normal_bias: f32,
    pub(super) snap_resolution: [f32; 2],
    pub(super) snap_enabled: u32,
    pub(super) affine_enabled: u32,
    pub(super) fog_color: [f32; 3],
    pub(super) fog_enabled: u32,
    pub(super) fog_start: f32,
    pub(super) fog_end: f32,
    pub(super) cascade_count: u32,
    pub(super) _padding2: f32,
    pub(super) cascade_view_projections:
        [[[f32; 4]; 4]; crate::render::wgpu::passes::NUM_SHADOW_CASCADES],
    pub(super) cascade_split_distances: [f32; crate::render::wgpu::passes::NUM_SHADOW_CASCADES],
    pub(super) cascade_atlas_offsets: [[f32; 4]; crate::render::wgpu::passes::NUM_SHADOW_CASCADES],
    pub(super) cascade_atlas_scale: [f32; 4],
    pub(super) time: f32,
    pub(super) _padding_align: [f32; 3],
    pub(super) directional_light_direction: [f32; 4],
    pub(super) _padding3: [f32; 20],
}

#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct ModelMatrix {
    pub(super) model: [[f32; 4]; 4],
    pub(super) normal_matrix: [[f32; 4]; 3],
}

#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct ObjectData {
    pub(super) transform_index: u32,
    pub(super) mesh_id: u32,
    pub(super) material_id: u32,
    pub(super) batch_id: u32,
    pub(super) morph_weights: [f32; 8],
    pub(super) morph_target_count: u32,
    pub(super) morph_displacement_offset: u32,
    pub(super) mesh_vertex_offset: u32,
    pub(super) mesh_vertex_count: u32,
}

#[repr(C, align(16))]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct CullingUniforms {
    pub(super) frustum_planes: [[f32; 4]; 6],
    pub(super) object_count: u32,
    pub(super) _padding: [u32; 3],
}

#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MaterialData {
    pub base_color: [f32; 4],
    pub emissive_factor: [f32; 3],
    pub alpha_mode: u32,
    pub alpha_cutoff: f32,
    pub has_base_texture: u32,
    pub has_emissive_texture: u32,
    pub has_normal_texture: u32,
    pub has_metallic_roughness_texture: u32,
    pub has_occlusion_texture: u32,
    pub normal_scale: f32,
    pub occlusion_strength: f32,
    pub roughness: f32,
    pub metallic: f32,
    pub unlit: u32,
    pub normal_map_flags: u32,
    pub uv_scale: [f32; 2],
    pub transmission_factor: f32,
    pub has_transmission_texture: u32,
    pub thickness: f32,
    pub has_thickness_texture: u32,
    pub _align_attenuation: [u32; 2],
    pub attenuation_color: [f32; 3],
    pub attenuation_distance: f32,
    pub ior: f32,
    pub specular_factor: f32,
    pub _align_specular: [u32; 2],
    pub specular_color_factor: [f32; 3],
    pub has_specular_texture: u32,
    pub has_specular_color_texture: u32,
    pub emissive_strength: f32,
    pub _padding: [u32; 2],
}

pub const NORMAL_MAP_FLIP_Y: u32 = 1;
pub const NORMAL_MAP_TWO_COMPONENT: u32 = 2;

#[derive(Clone, PartialEq, Default)]
pub struct MaterialTextures {
    pub base_texture: Option<String>,
    pub emissive_texture: Option<String>,
    pub normal_texture: Option<String>,
    pub metallic_roughness_texture: Option<String>,
    pub occlusion_texture: Option<String>,
    pub transmission_texture: Option<String>,
    pub thickness_texture: Option<String>,
    pub specular_texture: Option<String>,
    pub specular_color_texture: Option<String>,
}

impl MaterialTextures {
    pub(super) fn contains_any(&self, names: &HashSet<String>) -> bool {
        self.base_texture
            .as_ref()
            .is_some_and(|name| names.contains(name))
            || self
                .emissive_texture
                .as_ref()
                .is_some_and(|name| names.contains(name))
            || self
                .normal_texture
                .as_ref()
                .is_some_and(|name| names.contains(name))
            || self
                .metallic_roughness_texture
                .as_ref()
                .is_some_and(|name| names.contains(name))
            || self
                .occlusion_texture
                .as_ref()
                .is_some_and(|name| names.contains(name))
            || self
                .transmission_texture
                .as_ref()
                .is_some_and(|name| names.contains(name))
            || self
                .thickness_texture
                .as_ref()
                .is_some_and(|name| names.contains(name))
            || self
                .specular_texture
                .as_ref()
                .is_some_and(|name| names.contains(name))
            || self
                .specular_color_texture
                .as_ref()
                .is_some_and(|name| names.contains(name))
    }
}

#[derive(Debug, Clone)]
pub(super) struct MeshData {
    pub(super) vertex_offset: u32,
    pub(super) vertex_count: u32,
    pub(super) index_offset: u32,
    pub(super) index_count: u32,
    pub(super) morph_displacement_offset: u32,
    pub(super) morph_target_count: u32,
}

#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct MeshBoundsData {
    pub(super) center: [f32; 3],
    pub(super) radius: f32,
}

#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct MorphDisplacement {
    pub(super) position: [f32; 3],
    pub(super) _pad0: f32,
    pub(super) normal: [f32; 3],
    pub(super) _pad1: f32,
    pub(super) tangent: [f32; 3],
    pub(super) _pad2: f32,
}

#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct DrawIndexedIndirect {
    pub(super) index_count: u32,
    pub(super) instance_count: u32,
    pub(super) first_index: u32,
    pub(super) base_vertex: i32,
    pub(super) first_instance: u32,
}

pub(super) fn compute_normal_matrix(model_matrix: &nalgebra_glm::Mat4) -> [[f32; 4]; 3] {
    let mat3 = nalgebra_glm::mat4_to_mat3(model_matrix);

    let scale_x_sq =
        mat3[(0, 0)] * mat3[(0, 0)] + mat3[(1, 0)] * mat3[(1, 0)] + mat3[(2, 0)] * mat3[(2, 0)];
    let scale_y_sq =
        mat3[(0, 1)] * mat3[(0, 1)] + mat3[(1, 1)] * mat3[(1, 1)] + mat3[(2, 1)] * mat3[(2, 1)];
    let scale_z_sq =
        mat3[(0, 2)] * mat3[(0, 2)] + mat3[(1, 2)] * mat3[(1, 2)] + mat3[(2, 2)] * mat3[(2, 2)];

    let is_uniform_scale =
        (scale_x_sq - scale_y_sq).abs() < 0.001 && (scale_y_sq - scale_z_sq).abs() < 0.001;

    let normal_mat3 = if is_uniform_scale {
        let inv_scale = 1.0 / scale_x_sq.sqrt();
        nalgebra_glm::Mat3::new(
            mat3[(0, 0)] * inv_scale,
            mat3[(0, 1)] * inv_scale,
            mat3[(0, 2)] * inv_scale,
            mat3[(1, 0)] * inv_scale,
            mat3[(1, 1)] * inv_scale,
            mat3[(1, 2)] * inv_scale,
            mat3[(2, 0)] * inv_scale,
            mat3[(2, 1)] * inv_scale,
            mat3[(2, 2)] * inv_scale,
        )
    } else {
        match mat3.try_inverse() {
            Some(inv) => inv.transpose(),
            None => nalgebra_glm::Mat3::identity(),
        }
    };

    [
        [
            normal_mat3[(0, 0)],
            normal_mat3[(1, 0)],
            normal_mat3[(2, 0)],
            0.0,
        ],
        [
            normal_mat3[(0, 1)],
            normal_mat3[(1, 1)],
            normal_mat3[(2, 1)],
            0.0,
        ],
        [
            normal_mat3[(0, 2)],
            normal_mat3[(1, 2)],
            normal_mat3[(2, 2)],
            0.0,
        ],
    ]
}
