use super::super::types::{
    BUFFER_GROWTH_FACTOR, CLUSTER_GRID_X, CLUSTER_GRID_Y, CLUSTER_GRID_Z, ClusterUniforms,
    CullingUniforms, MAX_LIGHTS, MeshUniforms, cluster_slice_near,
};
use super::MeshPass;

impl MeshPass {
    pub(in super::super) fn prepare_uniforms_and_lights(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        world: &crate::ecs::world::World,
    ) {
        use super::super::super::projection::*;

        let uptime_ms = world.resources.window.timing.uptime_milliseconds;
        let cache_valid = self.per_frame_cache.valid && self.per_frame_cache.uptime_ms == uptime_ms;

        let (
            lights_data,
            num_directional_lights,
            directional_light_direction,
            directional_light,
            entity_to_lights_index,
        ) = if cache_valid {
            let _span = tracing::info_span!("mesh_prepare_lights_cached").entered();
            (
                self.per_frame_cache.lights_data.clone(),
                self.per_frame_cache.num_directional_lights,
                self.per_frame_cache.directional_light_direction,
                self.per_frame_cache.directional_light_transform.clone(),
                self.per_frame_cache.entity_to_lights_index.clone(),
            )
        } else {
            let _span = tracing::info_span!("mesh_prepare_lights_collect").entered();
            let light_result = collect_lights(world, MAX_LIGHTS);
            let dir = light_result
                .directional_light
                .as_ref()
                .map(|(_light, transform)| {
                    let dir = transform.forward_vector();
                    [dir.x, dir.y, dir.z, 0.0]
                })
                .unwrap_or([0.0, -1.0, 0.0, 0.0]);
            self.per_frame_cache.uptime_ms = uptime_ms;
            self.per_frame_cache.valid = true;
            self.per_frame_cache.lights_data = light_result.lights_data.clone();
            self.per_frame_cache.num_directional_lights = light_result.num_directional_lights;
            self.per_frame_cache.directional_light_direction = dir;
            self.per_frame_cache.directional_light_transform =
                light_result.directional_light.clone();
            self.per_frame_cache.entity_to_lights_index = light_result.entity_to_index.clone();
            (
                light_result.lights_data,
                light_result.num_directional_lights,
                dir,
                light_result.directional_light,
                light_result.entity_to_index,
            )
        };
        let mut lights_data = lights_data;
        {
            let state = self.world_states[self.current_world_id as usize]
                .as_mut()
                .unwrap();
            state.num_directional_lights = num_directional_lights;
            state.num_total_lights = lights_data.len() as u32;
        }

        let shadow_view_required = world
            .resources
            .renderer_state
            .active_view
            .shadow_depth_enabled
            && directional_light.is_some();
        let (
            cascade_view_projections,
            cascade_diameters,
            cascade_split_distances,
            light_view_projection,
            shadow_bias,
            shadows_enabled,
        ) = if shadow_view_required {
            let _span = tracing::info_span!("mesh_prepare_cascade").entered();
            let cascade_result = calculate_cascade_shadows(world, directional_light.as_ref());
            (
                cascade_result.cascade_view_projections,
                cascade_result.cascade_diameters,
                cascade_result.cascade_split_distances,
                cascade_result.light_view_projection,
                cascade_result.shadow_bias,
                cascade_result.shadows_enabled,
            )
        } else {
            (
                [[[0.0f32; 4]; 4]; crate::render::wgpu::passes::NUM_SHADOW_CASCADES],
                [0.0f32; crate::render::wgpu::passes::NUM_SHADOW_CASCADES],
                CASCADE_SPLIT_DISTANCES,
                [[0.0f32; 4]; 4],
                0.0,
                0.0,
            )
        };

        let cascade_texture_resolution = if cfg!(target_arch = "wasm32") {
            2048.0
        } else {
            4096.0
        };

        let cascade_atlas_offsets: [[f32; 4]; crate::render::wgpu::passes::NUM_SHADOW_CASCADES] = [
            [
                0.0,
                0.0,
                cascade_diameters[0] / cascade_texture_resolution,
                0.0,
            ],
            [
                0.5,
                0.0,
                cascade_diameters[1] / cascade_texture_resolution,
                0.0,
            ],
            [
                0.0,
                0.5,
                cascade_diameters[2] / cascade_texture_resolution,
                0.0,
            ],
            [
                0.5,
                0.5,
                cascade_diameters[3] / cascade_texture_resolution,
                0.0,
            ],
        ];

        if let Some(camera_matrices) =
            crate::ecs::camera::queries::query_active_camera_matrices(world)
        {
            let global_unlit = if world.resources.renderer_state.active_view.unlit_mode {
                1.0
            } else {
                0.0
            };

            let (snap_resolution, snap_enabled) =
                if let Some(ref vertex_snap) = world.resources.render_settings.vertex_snap {
                    (vertex_snap.resolution, 1)
                } else {
                    ([320.0, 240.0], 0)
                };

            let affine_enabled = if world.resources.render_settings.affine_texture_mapping {
                1
            } else {
                0
            };

            let (fog_color, fog_enabled, fog_start, fog_end) =
                if let Some(ref fog) = world.resources.renderer_state.active_view.fog {
                    (fog.color, 1, fog.start, fog.end)
                } else {
                    ([0.5, 0.5, 0.6], 0, 5.0, 30.0)
                };

            let time = world.resources.window.timing.uptime_milliseconds as f32 / 1000.0;

            let camera_z_far = world
                .resources
                .active_camera
                .and_then(|entity| world.core.get_camera(entity))
                .map(|camera| match &camera.projection {
                    crate::ecs::camera::components::Projection::Perspective(persp) => {
                        persp.z_far.unwrap_or(1000.0)
                    }
                    crate::ecs::camera::components::Projection::Orthographic(ortho) => ortho.z_far,
                })
                .unwrap_or(1000.0);
            let oit_z_scale = (camera_z_far * 0.2).max(1.0);

            let uniforms = MeshUniforms {
                view: camera_matrices.view.into(),
                projection: camera_matrices.projection.into(),
                camera_position: [
                    camera_matrices.camera_position.x,
                    camera_matrices.camera_position.y,
                    camera_matrices.camera_position.z,
                    1.0,
                ],
                num_lights: [lights_data.len() as u32, 0, 0, 0],
                ambient_light: world.resources.renderer_state.active_view.ambient_light,
                light_view_projection,
                shadow_bias,
                shadows_enabled,
                global_unlit,
                shadow_normal_bias: 1.8,
                snap_resolution,
                snap_enabled,
                affine_enabled,
                fog_color,
                fog_enabled,
                fog_start,
                fog_end,
                cascade_count: crate::render::wgpu::passes::NUM_SHADOW_CASCADES as u32,
                directional_light_size: 1.0,
                cascade_view_projections,
                cascade_split_distances,
                cascade_atlas_offsets,
                cascade_atlas_scale: [0.5, 0.5, 0.0, 0.0],
                time,
                pbr_debug_mode: world.resources.debug_draw.pbr_debug_mode.as_u32(),
                texture_debug_stripes: world.resources.debug_draw.texture_debug_stripes as u32,
                texture_debug_stripes_speed: world.resources.debug_draw.texture_debug_stripes_speed,
                directional_light_direction,
                ibl_blend_factor: 0.0,
                oit_z_scale,
                _pad_pre_flat: [0.0; 2],
                flat_color: world
                    .resources
                    .renderer_state
                    .active_view
                    .flat_shading_color
                    .map(|c| [c.x, c.y, c.z, c.w])
                    .unwrap_or([0.0; 4]),
                _padding3: [0.0; 12],
            };
            queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));

            let overlay_uniforms = MeshUniforms {
                view: camera_matrices.view.into(),
                projection: camera_matrices.projection.into(),
                camera_position: [
                    camera_matrices.camera_position.x,
                    camera_matrices.camera_position.y,
                    camera_matrices.camera_position.z,
                    1.0,
                ],
                num_lights: [lights_data.len() as u32, 0, 0, 0],
                ambient_light: world.resources.renderer_state.active_view.ambient_light,
                light_view_projection,
                shadow_bias,
                shadows_enabled: 0.0,
                global_unlit,
                shadow_normal_bias: 1.8,
                snap_resolution,
                snap_enabled,
                affine_enabled,
                fog_color,
                fog_enabled,
                fog_start,
                fog_end,
                cascade_count: crate::render::wgpu::passes::NUM_SHADOW_CASCADES as u32,
                directional_light_size: 1.0,
                cascade_view_projections,
                cascade_split_distances,
                cascade_atlas_offsets,
                cascade_atlas_scale: [0.5, 0.5, 0.0, 0.0],
                time,
                pbr_debug_mode: world.resources.debug_draw.pbr_debug_mode.as_u32(),
                texture_debug_stripes: world.resources.debug_draw.texture_debug_stripes as u32,
                texture_debug_stripes_speed: world.resources.debug_draw.texture_debug_stripes_speed,
                directional_light_direction,
                ibl_blend_factor: 0.0,
                oit_z_scale,
                _pad_pre_flat: [0.0; 2],
                flat_color: world
                    .resources
                    .renderer_state
                    .active_view
                    .flat_shading_color
                    .map(|c| [c.x, c.y, c.z, c.w])
                    .unwrap_or([0.0; 4]),
                _padding3: [0.0; 12],
            };
            queue.write_buffer(
                &self.overlay_uniform_buffer,
                0,
                bytemuck::cast_slice(&[overlay_uniforms]),
            );

            let view_proj = camera_matrices.projection * camera_matrices.view;
            let (culling_frustum_planes, culling_view_proj) = if let Some(culling_camera) =
                world.resources.editor_selection.culling_camera_override
            {
                if let Some(culling_matrices) =
                    crate::ecs::camera::queries::query_camera_matrices(world, culling_camera)
                {
                    let culling_vp = culling_matrices.projection * culling_matrices.view;
                    (extract_frustum_planes(&culling_vp), culling_vp)
                } else {
                    (extract_frustum_planes(&view_proj), view_proj)
                }
            } else {
                (extract_frustum_planes(&view_proj), view_proj)
            };

            let (screen_width, screen_height) = world
                .resources
                .window
                .cached_viewport_size
                .unwrap_or((1, 1));
            let projection_scale_y = camera_matrices.projection[(1, 1)];
            let min_screen_pixel_size = world.resources.renderer_state.min_screen_pixel_size;

            let object_count = self.world_states[self.current_world_id as usize]
                .as_ref()
                .unwrap()
                .object_count;

            let culling_uniforms = CullingUniforms {
                frustum_planes: culling_frustum_planes.map(|v| [v.x, v.y, v.z, v.w]),
                view_projection: culling_view_proj.into(),
                occluder_view_projection: view_proj.into(),
                screen_size: [screen_width as f32, screen_height as f32],
                object_count,
                min_screen_pixel_size,
                projection_scale_y,
                camera_culling_mask: world.resources.renderer_state.active_view.culling_mask,
                hiz_mip_count: self.hiz.mip_count() as f32,
                occlusion_enabled: u32::from(
                    self.hiz.hiz_view().is_some()
                        && world.resources.renderer_state.occlusion_culling_enabled
                        && world.resources.renderer_state.gpu_culling_enabled,
                ),
                frustum_enabled: u32::from(world.resources.renderer_state.gpu_culling_enabled),
                _pad_culling: [0; 3],
            };
            queue.write_buffer(
                &self.culling_uniform_buffer,
                0,
                bytemuck::cast_slice(&[culling_uniforms]),
            );

            let world_state = self.world_states[self.current_world_id as usize]
                .as_mut()
                .unwrap();
            let gpu = world_state.gpu_buffers.as_mut().unwrap();
            if gpu.culling_bind_group.is_none() {
                gpu.culling_bind_group =
                    Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
                        label: Some("Mesh Culling Bind Group"),
                        layout: &self.culling_bind_group_layout,
                        entries: &[
                            wgpu::BindGroupEntry {
                                binding: 0,
                                resource: gpu.transform_buffer.as_entire_binding(),
                            },
                            wgpu::BindGroupEntry {
                                binding: 1,
                                resource: gpu.object_buffer.as_entire_binding(),
                            },
                            wgpu::BindGroupEntry {
                                binding: 2,
                                resource: self.culling_uniform_buffer.as_entire_binding(),
                            },
                            wgpu::BindGroupEntry {
                                binding: 3,
                                resource: self.mesh_bounds_buffer.as_entire_binding(),
                            },
                            wgpu::BindGroupEntry {
                                binding: 4,
                                resource: gpu.indirect_buffer.as_entire_binding(),
                            },
                            wgpu::BindGroupEntry {
                                binding: 5,
                                resource: gpu.visible_indices_buffer.as_entire_binding(),
                            },
                            wgpu::BindGroupEntry {
                                binding: 6,
                                resource: self.mesh_lod_buffer.as_entire_binding(),
                            },
                            wgpu::BindGroupEntry {
                                binding: 7,
                                resource: wgpu::BindingResource::TextureView(
                                    self.hiz.hiz_view_or_dummy(),
                                ),
                            },
                        ],
                    }));
            }

            let (z_near, z_far) = world
                .resources
                .active_camera
                .and_then(|entity| world.core.get_camera(entity))
                .map(|camera| match &camera.projection {
                    crate::ecs::camera::components::Projection::Perspective(persp) => {
                        (persp.z_near, persp.z_far.unwrap_or(1000.0))
                    }
                    crate::ecs::camera::components::Projection::Orthographic(ortho) => {
                        (ortho.z_near, ortho.z_far)
                    }
                })
                .unwrap_or((0.1, 1000.0));

            let tile_size_x = (screen_width as f32) / (CLUSTER_GRID_X as f32);
            let tile_size_y = (screen_height as f32) / (CLUSTER_GRID_Y as f32);

            let inverse_projection: [[f32; 4]; 4] =
                nalgebra_glm::inverse(&camera_matrices.projection).into();

            let num_directional_lights = self.world_states[self.current_world_id as usize]
                .as_ref()
                .unwrap()
                .num_directional_lights;
            let cluster_uniforms = ClusterUniforms {
                inverse_projection,
                screen_size: [screen_width as f32, screen_height as f32],
                z_near: cluster_slice_near(z_near),
                z_far,
                cluster_count: [CLUSTER_GRID_X, CLUSTER_GRID_Y, CLUSTER_GRID_Z, 0],
                tile_size: [tile_size_x, tile_size_y],
                num_lights: lights_data.len() as u32,
                num_directional_lights,
            };
            let cluster_uniforms_buffer = &self.world_states[self.current_world_id as usize]
                .as_ref()
                .unwrap()
                .gpu_buffers
                .as_ref()
                .unwrap()
                .cluster_uniforms_buffer;
            queue.write_buffer(
                cluster_uniforms_buffer,
                0,
                bytemuck::cast_slice(&[cluster_uniforms]),
            );

            let view_matrix: [[f32; 4]; 4] = camera_matrices.view.into();
            queue.write_buffer(
                &self.view_matrix_buffer,
                0,
                bytemuck::cast_slice(&[view_matrix]),
            );

            let view_proj_hash = {
                use std::hash::{Hash, Hasher};
                let mut hasher = std::collections::hash_map::DefaultHasher::new();
                for row in &inverse_projection {
                    for val in row {
                        val.to_bits().hash(&mut hasher);
                    }
                }
                screen_width.hash(&mut hasher);
                screen_height.hash(&mut hasher);
                hasher.finish()
            };

            {
                let state = self.world_states[self.current_world_id as usize]
                    .as_mut()
                    .unwrap();
                state.camera_changed = view_proj_hash != state.last_camera_hash;
                if state.camera_changed {
                    state.last_camera_hash = view_proj_hash;
                }
            }

            let world_state = self.world_states[self.current_world_id as usize]
                .as_mut()
                .unwrap();
            let gpu = world_state.gpu_buffers.as_mut().unwrap();
            if gpu.cluster_assign_bind_group.is_none() {
                gpu.cluster_assign_bind_group =
                    Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
                        label: Some("Cluster Light Assign Bind Group (Per-World)"),
                        layout: &self.cluster_assign_bind_group_layout,
                        entries: &[
                            wgpu::BindGroupEntry {
                                binding: 0,
                                resource: gpu.cluster_uniforms_buffer.as_entire_binding(),
                            },
                            wgpu::BindGroupEntry {
                                binding: 1,
                                resource: self.cluster_bounds_buffer.as_entire_binding(),
                            },
                            wgpu::BindGroupEntry {
                                binding: 2,
                                resource: gpu.light_grid_buffer.as_entire_binding(),
                            },
                            wgpu::BindGroupEntry {
                                binding: 3,
                                resource: gpu.light_indices_buffer.as_entire_binding(),
                            },
                            wgpu::BindGroupEntry {
                                binding: 4,
                                resource: gpu.lights_buffer.as_entire_binding(),
                            },
                            wgpu::BindGroupEntry {
                                binding: 5,
                                resource: self.view_matrix_buffer.as_entire_binding(),
                            },
                        ],
                    }));
            }
        }

        let camera_position = world
            .resources
            .active_camera
            .and_then(|cam| world.core.get_global_transform(cam))
            .map(|t| nalgebra_glm::vec3(t.0[(0, 3)], t.0[(1, 3)], t.0[(2, 3)]))
            .unwrap_or_else(|| nalgebra_glm::vec3(0.0, 0.0, 0.0));

        let spotlight_result = collect_spotlight_shadows(world);
        apply_spotlight_shadow_indices(
            &mut lights_data,
            &spotlight_result.entity_to_shadow_index,
            &entity_to_lights_index,
        );

        if !spotlight_result.shadow_data.is_empty() {
            queue.write_buffer(
                &self.spotlight_shadow_buffer,
                0,
                bytemuck::cast_slice(&spotlight_result.shadow_data),
            );
        }

        let point_shadow_result = collect_point_light_shadows(
            world,
            camera_position,
            &mut lights_data,
            &entity_to_lights_index,
        );
        if !point_shadow_result.is_empty() {
            queue.write_buffer(
                &self.point_shadow_buffer,
                0,
                bytemuck::cast_slice(&point_shadow_result),
            );
        }

        if !lights_data.is_empty() {
            let lights_buffer_size = self.world_states[self.current_world_id as usize]
                .as_ref()
                .unwrap()
                .gpu_buffers
                .as_ref()
                .unwrap()
                .lights_buffer_size;
            if lights_data.len() > lights_buffer_size {
                let new_size = (lights_data.len() as f32 * BUFFER_GROWTH_FACTOR).ceil() as usize;
                let new_size = new_size.min(MAX_LIGHTS);

                let new_buffer = device.create_buffer(&wgpu::BufferDescriptor {
                    label: Some("Mesh Lights Buffer (Resized)"),
                    size: (std::mem::size_of::<LightData>() * new_size) as u64,
                    usage: wgpu::BufferUsages::STORAGE
                        | wgpu::BufferUsages::COPY_DST
                        | wgpu::BufferUsages::COPY_SRC,
                    mapped_at_creation: false,
                });

                {
                    let world_state = self.world_states[self.current_world_id as usize]
                        .as_mut()
                        .unwrap();
                    let gpu = world_state.gpu_buffers.as_mut().unwrap();
                    gpu.lights_buffer = new_buffer;
                    gpu.lights_buffer_size = new_size;
                }
                self.rebuild_instance_bind_group(device);
            }

            let lights_buffer = &self.world_states[self.current_world_id as usize]
                .as_ref()
                .unwrap()
                .gpu_buffers
                .as_ref()
                .unwrap()
                .lights_buffer;
            queue.write_buffer(lights_buffer, 0, bytemuck::cast_slice(&lights_data));
        }

        let mut area_result = collect_area_lights(world, MAX_AREA_LIGHTS);
        apply_area_light_shadow_indices(
            &mut area_result.area_lights_data,
            &spotlight_result.entity_to_shadow_index,
            &area_result.entity_to_index,
        );
        resolve_area_emissive_layers(
            world,
            &mut area_result.area_lights_data,
            &area_result.entity_to_index,
            &self.material_layer_map,
            &world.resources.texture_cache.registry,
        );
        {
            let gpu = self.world_states[self.current_world_id as usize]
                .as_ref()
                .unwrap()
                .gpu_buffers
                .as_ref()
                .unwrap();
            if !area_result.area_lights_data.is_empty() {
                queue.write_buffer(
                    &gpu.area_lights_buffer,
                    0,
                    bytemuck::cast_slice(&area_result.area_lights_data),
                );
            }
            let area_uniforms = AreaUniforms {
                count: area_result.area_lights_data.len() as u32,
                _pad0: 0,
                _pad1: 0,
                _pad2: 0,
            };
            queue.write_buffer(
                &gpu.area_uniforms_buffer,
                0,
                bytemuck::cast_slice(&[area_uniforms]),
            );
        }
    }
}
