pub(super) fn reverse_z_ortho_light(
    left: f32,
    right: f32,
    bottom: f32,
    top: f32,
    near: f32,
    far: f32,
) -> nalgebra_glm::Mat4 {
    let width = right - left;
    let height = top - bottom;
    let depth = far - near;

    nalgebra_glm::Mat4::new(
        2.0 / width,
        0.0,
        0.0,
        -(right + left) / width,
        0.0,
        2.0 / height,
        0.0,
        -(top + bottom) / height,
        0.0,
        0.0,
        1.0 / depth,
        -near / depth,
        0.0,
        0.0,
        0.0,
        1.0,
    )
}

pub(super) fn get_frustum_corners_world_space(
    view: &nalgebra_glm::Mat4,
    fov: f32,
    aspect: f32,
    near: f32,
    far: f32,
) -> [nalgebra_glm::Vec3; 8] {
    let inv_view = nalgebra_glm::inverse(view);
    let tan_half_fov = (fov / 2.0).tan();

    let near_height = near * tan_half_fov;
    let near_width = near_height * aspect;
    let far_height = far * tan_half_fov;
    let far_width = far_height * aspect;

    let corners_view = [
        nalgebra_glm::vec3(-near_width, -near_height, -near),
        nalgebra_glm::vec3(near_width, -near_height, -near),
        nalgebra_glm::vec3(near_width, near_height, -near),
        nalgebra_glm::vec3(-near_width, near_height, -near),
        nalgebra_glm::vec3(-far_width, -far_height, -far),
        nalgebra_glm::vec3(far_width, -far_height, -far),
        nalgebra_glm::vec3(far_width, far_height, -far),
        nalgebra_glm::vec3(-far_width, far_height, -far),
    ];

    let mut corners_world = [nalgebra_glm::vec3(0.0, 0.0, 0.0); 8];
    for (index, corner) in corners_view.iter().enumerate() {
        let world_pos = inv_view * nalgebra_glm::vec4(corner.x, corner.y, corner.z, 1.0);
        corners_world[index] = nalgebra_glm::vec3(world_pos.x, world_pos.y, world_pos.z);
    }
    corners_world
}

pub struct CascadeViewProjectionResult {
    pub view_projection: nalgebra_glm::Mat4,
    pub cascade_diameter: f32,
}

pub(super) fn calculate_cascade_view_projection(
    frustum_corners: &[nalgebra_glm::Vec3; 8],
    light_direction: &nalgebra_glm::Vec3,
    _cascade_resolution: f32,
    _cascade_far: f32,
) -> CascadeViewProjectionResult {
    let mut center = nalgebra_glm::vec3(0.0, 0.0, 0.0);
    for corner in frustum_corners {
        center += corner;
    }
    center /= 8.0;

    let mut max_radius = 0.0f32;
    for corner in frustum_corners {
        let dist = nalgebra_glm::length(&(corner - center));
        max_radius = max_radius.max(dist);
    }

    let up = if light_direction.y.abs() > 0.99 {
        nalgebra_glm::vec3(1.0, 0.0, 0.0)
    } else {
        nalgebra_glm::vec3(0.0, 1.0, 0.0)
    };

    let light_dir_normalized = light_direction.normalize();

    let light_view = nalgebra_glm::look_at(
        &(center - light_dir_normalized * max_radius * 4.0),
        &center,
        &up,
    );

    let mut min_x = f32::MAX;
    let mut max_x = f32::MIN;
    let mut min_y = f32::MAX;
    let mut max_y = f32::MIN;
    let mut min_z = f32::MAX;
    let mut max_z = f32::MIN;

    for corner in frustum_corners {
        let light_space = light_view * nalgebra_glm::vec4(corner.x, corner.y, corner.z, 1.0);
        min_x = min_x.min(light_space.x);
        max_x = max_x.max(light_space.x);
        min_y = min_y.min(light_space.y);
        max_y = max_y.max(light_space.y);
        min_z = min_z.min(light_space.z);
        max_z = max_z.max(light_space.z);
    }

    let padding = (max_x - min_x).max(max_y - min_y) * 0.1;
    min_x -= padding;
    max_x += padding;
    min_y -= padding;
    max_y += padding;

    let z_mult = 10.0;
    if min_z < 0.0 {
        min_z *= z_mult;
    } else {
        min_z /= z_mult;
    }
    if max_z < 0.0 {
        max_z /= z_mult;
    } else {
        max_z *= z_mult;
    }

    let light_projection = reverse_z_ortho_light(min_x, max_x, min_y, max_y, min_z, max_z);

    let cascade_diameter = max_radius * 2.0;

    CascadeViewProjectionResult {
        view_projection: light_projection * light_view,
        cascade_diameter,
    }
}

pub(super) fn reverse_z_perspective(
    fov: f32,
    aspect: f32,
    near: f32,
    far: f32,
) -> nalgebra_glm::Mat4 {
    let f = 1.0 / (fov / 2.0).tan();
    let depth = far - near;

    nalgebra_glm::Mat4::new(
        f / aspect,
        0.0,
        0.0,
        0.0,
        0.0,
        f,
        0.0,
        0.0,
        0.0,
        0.0,
        near / depth,
        near * far / depth,
        0.0,
        0.0,
        -1.0,
        0.0,
    )
}

pub(super) fn extract_frustum_planes(
    view_proj: &crate::ecs::world::Mat4,
) -> [crate::ecs::world::Vec4; 6] {
    let mut planes = [crate::ecs::world::Vec4::zeros(); 6];

    let row0 = crate::ecs::world::Vec4::new(
        view_proj[(0, 0)],
        view_proj[(0, 1)],
        view_proj[(0, 2)],
        view_proj[(0, 3)],
    );
    let row1 = crate::ecs::world::Vec4::new(
        view_proj[(1, 0)],
        view_proj[(1, 1)],
        view_proj[(1, 2)],
        view_proj[(1, 3)],
    );
    let row2 = crate::ecs::world::Vec4::new(
        view_proj[(2, 0)],
        view_proj[(2, 1)],
        view_proj[(2, 2)],
        view_proj[(2, 3)],
    );
    let row3 = crate::ecs::world::Vec4::new(
        view_proj[(3, 0)],
        view_proj[(3, 1)],
        view_proj[(3, 2)],
        view_proj[(3, 3)],
    );

    planes[0] = row3 + row0;
    planes[1] = row3 - row0;
    planes[2] = row3 + row1;
    planes[3] = row3 - row1;
    planes[4] = row3 + row2;
    planes[5] = row3 - row2;

    for plane in &mut planes {
        let normal_length = (plane.x * plane.x + plane.y * plane.y + plane.z * plane.z).sqrt();
        if normal_length > 1e-6 {
            *plane /= normal_length;
        }
    }

    planes
}

#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct LightData {
    pub position: [f32; 4],
    pub direction: [f32; 4],
    pub color: [f32; 4],
    pub light_type: u32,
    pub range: f32,
    pub inner_cone: f32,
    pub outer_cone: f32,
    pub shadow_index: i32,
    pub _padding: [f32; 3],
}

pub const CASCADE_SPLIT_DISTANCES: [f32; crate::render::wgpu::passes::NUM_SHADOW_CASCADES] =
    [10.0, 40.0, 150.0, 500.0];

pub const MAX_SPOTLIGHT_SHADOWS: usize = 16;

pub struct LightCollectionResult {
    pub lights_data: Vec<LightData>,
    pub directional_light: Option<(
        crate::ecs::light::components::Light,
        crate::ecs::transform::components::GlobalTransform,
    )>,
}

pub fn collect_lights(
    world: &crate::ecs::world::World,
    max_lights: usize,
) -> LightCollectionResult {
    let mut lights_data = Vec::new();
    let mut directional_light = None;

    for entity in
        world.query_entities(crate::ecs::world::LIGHT | crate::ecs::world::GLOBAL_TRANSFORM)
    {
        if let (Some(light), Some(transform)) =
            (world.get_light(entity), world.get_global_transform(entity))
        {
            let light_type = match light.light_type {
                crate::ecs::light::components::LightType::Directional => 0,
                crate::ecs::light::components::LightType::Point => 1,
                crate::ecs::light::components::LightType::Spot => 2,
            };

            if light_type == 0 && light.cast_shadows && directional_light.is_none() {
                directional_light = Some((light.clone(), *transform));
            }

            let position = transform.translation();
            let direction = transform.forward_vector();

            lights_data.push(LightData {
                position: [position.x, position.y, position.z, 1.0],
                direction: [direction.x, direction.y, direction.z, 0.0],
                color: [
                    light.color.x * light.intensity,
                    light.color.y * light.intensity,
                    light.color.z * light.intensity,
                    1.0,
                ],
                light_type,
                range: light.range,
                inner_cone: light.inner_cone_angle.cos(),
                outer_cone: light.outer_cone_angle.cos(),
                shadow_index: -1,
                _padding: [0.0; 3],
            });

            if lights_data.len() >= max_lights {
                break;
            }
        }
    }

    LightCollectionResult {
        lights_data,
        directional_light,
    }
}

pub struct CascadeShadowResult {
    pub cascade_view_projections: [[[f32; 4]; 4]; crate::render::wgpu::passes::NUM_SHADOW_CASCADES],
    pub cascade_diameters: [f32; crate::render::wgpu::passes::NUM_SHADOW_CASCADES],
    pub light_view_projection: [[f32; 4]; 4],
    pub shadow_bias: f32,
    pub shadows_enabled: f32,
}

pub fn calculate_cascade_shadows(
    world: &crate::ecs::world::World,
    directional_light: Option<&(
        crate::ecs::light::components::Light,
        crate::ecs::transform::components::GlobalTransform,
    )>,
) -> CascadeShadowResult {
    let mut cascade_view_projections =
        [[[0.0f32; 4]; 4]; crate::render::wgpu::passes::NUM_SHADOW_CASCADES];
    let mut cascade_diameters = [0.0f32; crate::render::wgpu::passes::NUM_SHADOW_CASCADES];

    let (light_view_projection, shadow_bias, shadows_enabled) =
        if let Some((_light, light_transform)) = directional_light {
            let light_direction = light_transform.forward_vector();

            let camera_matrices = crate::ecs::camera::queries::query_active_camera_matrices(world);

            if let Some(camera_matrices) = camera_matrices {
                let active_camera = world.resources.active_camera;
                let camera = active_camera.and_then(|entity| world.get_camera(entity));

                let (fov, aspect, camera_near) = if let Some(camera) = camera {
                    match &camera.projection {
                        crate::ecs::camera::components::Projection::Perspective(persp) => {
                            let aspect = persp.aspect_ratio.unwrap_or_else(|| {
                                crate::ecs::camera::queries::query_window_aspect_ratio(world)
                                    .unwrap_or(1.78)
                            });
                            (persp.y_fov_rad, aspect, persp.z_near)
                        }
                        crate::ecs::camera::components::Projection::Orthographic(_) => {
                            (std::f32::consts::FRAC_PI_4, 1.78, 0.1)
                        }
                    }
                } else {
                    (std::f32::consts::FRAC_PI_4, 1.78, 0.1)
                };

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

                for cascade_index in 0..crate::render::wgpu::passes::NUM_SHADOW_CASCADES {
                    let cascade_near = if cascade_index == 0 {
                        camera_near
                    } else {
                        CASCADE_SPLIT_DISTANCES[cascade_index - 1]
                    };
                    let cascade_far = CASCADE_SPLIT_DISTANCES[cascade_index];

                    let frustum_corners = get_frustum_corners_world_space(
                        &camera_matrices.view,
                        fov,
                        aspect,
                        cascade_near,
                        cascade_far,
                    );

                    let result = calculate_cascade_view_projection(
                        &frustum_corners,
                        &light_direction,
                        cascade_resolution,
                        cascade_far,
                    );

                    cascade_view_projections[cascade_index] = result.view_projection.into();
                    cascade_diameters[cascade_index] = result.cascade_diameter;
                }
            } else {
                let up = if light_direction.y.abs() > 0.99 {
                    nalgebra_glm::vec3(1.0, 0.0, 0.0)
                } else {
                    nalgebra_glm::vec3(0.0, 1.0, 0.0)
                };

                for cascade_index in 0..crate::render::wgpu::passes::NUM_SHADOW_CASCADES {
                    let cascade_far = CASCADE_SPLIT_DISTANCES[cascade_index];
                    let half_size = cascade_far * 0.5;

                    let scene_center = nalgebra_glm::vec3(0.0, 0.0, 0.0);
                    let light_position = scene_center - light_direction * 100.0;
                    let light_view = nalgebra_glm::look_at(&light_position, &scene_center, &up);
                    let light_projection = reverse_z_ortho_light(
                        -half_size,
                        half_size,
                        -half_size,
                        half_size,
                        0.1,
                        cascade_far * 2.0,
                    );

                    cascade_view_projections[cascade_index] =
                        (light_projection * light_view).into();
                    cascade_diameters[cascade_index] = cascade_far;
                }
            }

            (cascade_view_projections[0], 0.02, 1.0)
        } else {
            (nalgebra_glm::Mat4::identity().into(), 0.0, 0.0)
        };

    CascadeShadowResult {
        cascade_view_projections,
        cascade_diameters,
        light_view_projection,
        shadow_bias,
        shadows_enabled,
    }
}

pub struct SpotlightShadowResult {
    pub shadow_data: Vec<crate::render::wgpu::passes::SpotlightShadowData>,
    pub entity_to_shadow_index: std::collections::HashMap<crate::ecs::world::Entity, i32>,
}

pub fn collect_spotlight_shadows(
    world: &crate::ecs::world::World,
    camera_position: nalgebra_glm::Vec3,
) -> SpotlightShadowResult {
    let mut spotlight_candidates: Vec<(
        crate::ecs::world::Entity,
        f32,
        crate::ecs::light::components::Light,
        crate::ecs::transform::components::GlobalTransform,
    )> = Vec::new();

    for entity in
        world.query_entities(crate::ecs::world::LIGHT | crate::ecs::world::GLOBAL_TRANSFORM)
    {
        if let (Some(light), Some(transform)) =
            (world.get_light(entity), world.get_global_transform(entity))
            && matches!(
                light.light_type,
                crate::ecs::light::components::LightType::Spot
            )
            && light.cast_shadows
        {
            let light_pos = nalgebra_glm::vec3(
                transform.0[(0, 3)],
                transform.0[(1, 3)],
                transform.0[(2, 3)],
            );
            let distance_sq = nalgebra_glm::length2(&(light_pos - camera_position));
            spotlight_candidates.push((entity, distance_sq, light.clone(), *transform));
        }
    }

    spotlight_candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));

    let mut shadow_data: Vec<crate::render::wgpu::passes::SpotlightShadowData> = Vec::new();
    let mut entity_to_shadow_index: std::collections::HashMap<crate::ecs::world::Entity, i32> =
        std::collections::HashMap::new();

    let slot_scale = 1.0 / 4.0;

    for (slot_index, (entity, _distance, light, transform)) in spotlight_candidates
        .iter()
        .take(MAX_SPOTLIGHT_SHADOWS)
        .enumerate()
    {
        entity_to_shadow_index.insert(*entity, slot_index as i32);

        let light_position = nalgebra_glm::vec3(
            transform.0[(0, 3)],
            transform.0[(1, 3)],
            transform.0[(2, 3)],
        );
        let light_direction = transform.forward_vector();

        let up = if light_direction.y.abs() > 0.99 {
            nalgebra_glm::vec3(1.0, 0.0, 0.0)
        } else {
            nalgebra_glm::vec3(0.0, 1.0, 0.0)
        };

        let target = light_position + light_direction;
        let light_view = nalgebra_glm::look_at(&light_position, &target, &up);

        let fov = light.outer_cone_angle * 2.0;
        let near = 0.1;
        let far = light.range.max(1.0);
        let light_projection = reverse_z_perspective(fov, 1.0, near, far);

        let view_projection = light_projection * light_view;

        let slot_x = (slot_index as u32) % 4;
        let slot_y = (slot_index as u32) / 4;

        shadow_data.push(crate::render::wgpu::passes::SpotlightShadowData {
            view_projection: view_projection.into(),
            atlas_offset: [slot_x as f32 * slot_scale, slot_y as f32 * slot_scale],
            atlas_scale: [slot_scale, slot_scale],
            bias: light.shadow_bias,
            _padding: [0.0; 3],
        });
    }

    SpotlightShadowResult {
        shadow_data,
        entity_to_shadow_index,
    }
}

pub fn apply_spotlight_shadow_indices(
    world: &crate::ecs::world::World,
    lights_data: &mut [LightData],
    entity_to_shadow_index: &std::collections::HashMap<crate::ecs::world::Entity, i32>,
) {
    for (index, entity) in world
        .query_entities(crate::ecs::world::LIGHT | crate::ecs::world::GLOBAL_TRANSFORM)
        .enumerate()
    {
        if index < lights_data.len()
            && let Some(&shadow_index) = entity_to_shadow_index.get(&entity)
        {
            lights_data[index].shadow_index = shadow_index;
        }
    }
}

pub const MAX_POINT_LIGHT_SHADOWS: usize = 4;

pub fn collect_point_light_shadows(
    world: &crate::ecs::world::World,
    camera_position: nalgebra_glm::Vec3,
    lights_data: &mut [LightData],
) -> Vec<crate::render::wgpu::passes::shadow_depth::PointLightShadowData> {
    let mut point_light_candidates: Vec<(
        crate::ecs::world::Entity,
        f32,
        crate::ecs::light::components::Light,
        crate::ecs::transform::components::GlobalTransform,
    )> = Vec::new();

    for entity in
        world.query_entities(crate::ecs::world::LIGHT | crate::ecs::world::GLOBAL_TRANSFORM)
    {
        if let (Some(light), Some(transform)) =
            (world.get_light(entity), world.get_global_transform(entity))
            && matches!(
                light.light_type,
                crate::ecs::light::components::LightType::Point
            )
            && light.cast_shadows
        {
            let light_pos = nalgebra_glm::vec3(
                transform.0[(0, 3)],
                transform.0[(1, 3)],
                transform.0[(2, 3)],
            );
            let distance_sq = nalgebra_glm::length2(&(light_pos - camera_position));
            point_light_candidates.push((entity, distance_sq, light.clone(), *transform));
        }
    }

    point_light_candidates
        .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));

    let mut shadow_data: Vec<crate::render::wgpu::passes::shadow_depth::PointLightShadowData> =
        Vec::new();
    let mut entity_to_shadow_index: std::collections::HashMap<crate::ecs::world::Entity, i32> =
        std::collections::HashMap::new();

    for (slot_index, (entity, _distance, light, transform)) in point_light_candidates
        .iter()
        .take(MAX_POINT_LIGHT_SHADOWS)
        .enumerate()
    {
        entity_to_shadow_index.insert(*entity, slot_index as i32);

        let light_position = nalgebra_glm::vec3(
            transform.0[(0, 3)],
            transform.0[(1, 3)],
            transform.0[(2, 3)],
        );

        shadow_data.push(
            crate::render::wgpu::passes::shadow_depth::PointLightShadowData {
                position: [light_position.x, light_position.y, light_position.z],
                range: light.range.max(0.1),
                bias: light.shadow_bias,
                shadow_index: slot_index as i32,
                _padding: [0.0; 2],
            },
        );
    }

    for (index, entity) in world
        .query_entities(crate::ecs::world::LIGHT | crate::ecs::world::GLOBAL_TRANSFORM)
        .enumerate()
    {
        if index < lights_data.len()
            && let Some(&shadow_index) = entity_to_shadow_index.get(&entity)
        {
            lights_data[index].shadow_index = shadow_index;
        }
    }

    shadow_data
}
