pub mod brdf_lut;
pub mod envmap_filter;
pub mod font_atlas;
pub mod hdr;
pub mod passes;
pub mod rendergraph;
pub mod sprite_texture_atlas;
pub mod text_mesh;
pub mod texture_cache;

use crate::ecs::generational_registry::registry_entry_by_name;
use texture_cache::texture_cache_load_from_raw_rgba;

#[cfg(feature = "egui")]
pub struct CameraViewport {
    pub texture: wgpu::Texture,
    pub view: wgpu::TextureView,
    pub texture_id: egui::TextureId,
    pub size: (u32, u32),
}

#[cfg(not(feature = "egui"))]
pub struct CameraViewport {
    pub texture: wgpu::Texture,
    pub view: wgpu::TextureView,
    pub size: (u32, u32),
}

const DEPTH_PICK_SAMPLE_SIZE: u32 = 5;

pub struct WgpuRenderer {
    surface: wgpu::Surface<'static>,
    device: wgpu::Device,
    queue: wgpu::Queue,
    surface_config: wgpu::SurfaceConfiguration,
    surface_format: wgpu::TextureFormat,
    graph: rendergraph::RenderGraph<crate::ecs::world::World>,
    depth_id: rendergraph::ResourceId,
    scene_color_id: rendergraph::ResourceId,
    compute_output_id: rendergraph::ResourceId,
    swapchain_id: rendergraph::ResourceId,
    viewport_resource_id: rendergraph::ResourceId,
    selection_mask_id: rendergraph::ResourceId,
    ui_depth_id: rendergraph::ResourceId,
    entity_id_id: rendergraph::ResourceId,
    #[cfg(feature = "egui")]
    egui_pass: Option<Box<passes::EguiPass>>,
    font_textures: Vec<(wgpu::Texture, wgpu::TextureView)>,
    font_data: Vec<crate::ecs::text::resources::FontAtlasData>,
    font_initialized: bool,
    camera_viewports: std::collections::HashMap<freecs::Entity, CameraViewport>,
    _brdf_lut_texture: wgpu::Texture,
    brdf_lut_view: wgpu::TextureView,
    depth_pick_compute_pipeline: wgpu::ComputePipeline,
    depth_pick_bind_group_layout: wgpu::BindGroupLayout,
    depth_pick_storage_buffer: wgpu::Buffer,
    depth_pick_uniform_buffer: wgpu::Buffer,
    depth_pick_staging_buffer: wgpu::Buffer,
    depth_pick_bind_group: Option<wgpu::BindGroup>,
    depth_pick_pending: bool,
    depth_pick_map_complete: std::sync::Arc<std::sync::atomic::AtomicBool>,
    depth_pick_center: (u32, u32),
}

impl WgpuRenderer {
    #[cfg(feature = "egui")]
    fn ensure_camera_viewport(
        &mut self,
        camera_entity: freecs::Entity,
        width: u32,
        height: u32,
    ) -> Option<egui::TextureId> {
        if let Some(viewport) = self.camera_viewports.get(&camera_entity)
            && viewport.size == (width, height)
        {
            return Some(viewport.texture_id);
        }

        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
            label: Some(&format!("Viewport Texture (Camera {})", camera_entity.id)),
            size: wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: self.surface_format,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[],
        });

        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());

        let texture_id = if let Some(egui_pass) = self.graph.get_pass_mut("egui_pass")
            && let Some(egui_pass) =
                (egui_pass as &mut dyn std::any::Any).downcast_mut::<passes::EguiPass>()
        {
            egui_pass.register_texture(&self.device, &view)
        } else {
            return None;
        };

        self.camera_viewports.insert(
            camera_entity,
            CameraViewport {
                texture,
                view,
                texture_id,
                size: (width, height),
            },
        );

        Some(texture_id)
    }

    fn cleanup_unused_camera_viewports(&mut self, required_cameras: &[freecs::Entity]) {
        self.camera_viewports
            .retain(|entity, _| required_cameras.contains(entity));
    }

    pub async fn new_async<W>(
        window_handle: W,
        initial_width: u32,
        initial_height: u32,
    ) -> Result<Self, Box<dyn std::error::Error>>
    where
        W: Into<wgpu::SurfaceTarget<'static>>,
    {
        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
        let surface = instance.create_surface(window_handle)?;

        let adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                power_preference: wgpu::PowerPreference::default(),
                compatible_surface: Some(&surface),
                force_fallback_adapter: false,
            })
            .await
            .expect("Failed to request adapter!");

        let mut required_features = wgpu::Features::INDIRECT_FIRST_INSTANCE;

        if !cfg!(target_os = "macos") {
            required_features |= wgpu::Features::MULTI_DRAW_INDIRECT_COUNT;
        }

        if !cfg!(target_arch = "wasm32") {
            required_features |= wgpu::Features::POLYGON_MODE_LINE;
        }

        let mut required_limits = wgpu::Limits::default().using_resolution(adapter.limits());
        required_limits.max_bind_groups = if cfg!(target_arch = "wasm32") { 4 } else { 8 };

        let (device, queue) = adapter
            .request_device(&wgpu::DeviceDescriptor {
                label: Some("WGPU Device"),
                memory_hints: wgpu::MemoryHints::default(),
                required_features,
                required_limits,
                experimental_features: wgpu::ExperimentalFeatures::disabled(),
                trace: wgpu::Trace::Off,
            })
            .await?;

        let surface_capabilities = surface.get_capabilities(&adapter);

        let surface_format = surface_capabilities
            .formats
            .iter()
            .copied()
            .find(|f| !f.is_srgb())
            .unwrap_or(surface_capabilities.formats[0]);

        let surface_config = wgpu::SurfaceConfiguration {
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            format: surface_format,
            width: initial_width,
            height: initial_height,
            present_mode: surface_capabilities.present_modes[0],
            alpha_mode: surface_capabilities.alpha_modes[0],
            view_formats: vec![],
            desired_maximum_frame_latency: 2,
        };

        surface.configure(&device, &surface_config);

        let depth_format = wgpu::TextureFormat::Depth32Float;
        let hdr_format = wgpu::TextureFormat::Rgba16Float;
        let clear_pass = passes::ClearPass::new();
        let sky_pass = passes::SkyPass::new(&device, hdr_format, depth_format);
        let grid_pass = passes::GridPass::new(&device, hdr_format, depth_format);
        let scene_pass = passes::ScenePass::new(&device, hdr_format, depth_format);

        let shadow_depth_pass = passes::ShadowDepthPass::new(&device, &queue);

        let mesh_pass = passes::MeshPass::new(
            &device,
            &queue,
            hdr_format,
            depth_format,
            (initial_width, initial_height),
        );

        let skinned_mesh_pass =
            passes::SkinnedMeshPass::new(&device, &queue, hdr_format, depth_format);

        let water_pass = passes::WaterPass::new(&device, hdr_format, depth_format);

        let water_mesh_pass = passes::WaterMeshPass::new(&device, hdr_format, depth_format);

        let lines_pass = passes::LinesPass::new(&device, hdr_format);

        let sprite_pass = passes::SpritePass::new(&device, hdr_format, &queue);

        let selection_mask_pass = passes::SelectionMaskPass::new(&device, depth_format);

        #[cfg(feature = "egui")]
        let egui_pass = passes::EguiPass::new(&device, surface_format);

        let mut graph = rendergraph::RenderGraph::new();

        let depth_id = graph
            .add_depth_texture("depth")
            .size(initial_width.max(1), initial_height.max(1))
            .usage(
                wgpu::TextureUsages::RENDER_ATTACHMENT
                    | wgpu::TextureUsages::TEXTURE_BINDING
                    | wgpu::TextureUsages::COPY_SRC,
            )
            .clear_depth(0.0)
            .transient();

        let scene_color_id = graph
            .add_color_texture("scene_color")
            .format(wgpu::TextureFormat::Rgba16Float)
            .size(initial_width.max(1), initial_height.max(1))
            .clear_color(wgpu::Color {
                r: 0.0,
                g: 0.0,
                b: 0.0,
                a: 1.0,
            })
            .transient();

        let compute_output_id = graph
            .add_color_texture("compute_output")
            .format(surface_format)
            .size(initial_width.max(1), initial_height.max(1))
            .usage(wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT)
            .transient();

        let swapchain_id = graph
            .add_color_texture("swapchain")
            .format(surface_format)
            .external();

        let viewport_resource_id = graph
            .add_color_texture("viewport_output")
            .format(surface_format)
            .external();

        let shadow_map_size = if cfg!(target_arch = "wasm32") {
            4096
        } else {
            8192
        };
        let shadow_depth_id = graph
            .add_depth_texture("shadow_depth")
            .size(shadow_map_size, shadow_map_size)
            .format(wgpu::TextureFormat::Depth32Float)
            .clear_depth(0.0)
            .transient();

        let spotlight_shadow_atlas_size = if cfg!(target_arch = "wasm32") {
            1024
        } else {
            4096
        };
        let spotlight_shadow_atlas_id = graph
            .add_depth_texture("spotlight_shadow_atlas")
            .size(spotlight_shadow_atlas_size, spotlight_shadow_atlas_size)
            .format(wgpu::TextureFormat::Depth32Float)
            .clear_depth(0.0)
            .transient();

        let selection_mask_id = graph
            .add_color_texture("selection_mask")
            .format(wgpu::TextureFormat::R8Unorm)
            .size(initial_width.max(1), initial_height.max(1))
            .clear_color(wgpu::Color::TRANSPARENT)
            .transient();

        let entity_id_id = graph
            .add_color_texture("entity_id")
            .format(wgpu::TextureFormat::R32Float)
            .size(initial_width.max(1), initial_height.max(1))
            .usage(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING)
            .clear_color(wgpu::Color::TRANSPARENT)
            .transient();

        let ui_depth_id = graph
            .add_depth_texture("ui_depth")
            .size(initial_width.max(1), initial_height.max(1))
            .format(wgpu::TextureFormat::Depth32Float)
            .clear_depth(0.0)
            .transient();

        graph.add_pass(
            Box::new(clear_pass),
            &[("color", scene_color_id), ("depth", depth_id)],
        )?;

        graph.add_pass(Box::new(sky_pass), &[("color", scene_color_id)])?;

        graph.add_pass(
            Box::new(scene_pass),
            &[("color", scene_color_id), ("depth", depth_id)],
        )?;

        graph.add_pass(
            Box::new(shadow_depth_pass),
            &[
                ("shadow_depth", shadow_depth_id),
                ("spotlight_shadow_atlas", spotlight_shadow_atlas_id),
            ],
        )?;

        graph.add_pass(
            Box::new(mesh_pass),
            &[
                ("color", scene_color_id),
                ("depth", depth_id),
                ("shadow_depth", shadow_depth_id),
                ("spotlight_shadow_atlas", spotlight_shadow_atlas_id),
                ("entity_id", entity_id_id),
            ],
        )?;

        graph.add_pass(
            Box::new(skinned_mesh_pass),
            &[
                ("color", scene_color_id),
                ("depth", depth_id),
                ("shadow_depth", shadow_depth_id),
                ("spotlight_shadow_atlas", spotlight_shadow_atlas_id),
            ],
        )?;

        graph.add_pass(
            Box::new(water_pass),
            &[("color", scene_color_id), ("depth", depth_id)],
        )?;

        graph.add_pass(
            Box::new(water_mesh_pass),
            &[("color", scene_color_id), ("depth", depth_id)],
        )?;

        graph.add_pass(
            Box::new(grid_pass),
            &[("color", scene_color_id), ("depth", depth_id)],
        )?;

        graph.add_pass(
            Box::new(lines_pass),
            &[("color", scene_color_id), ("depth", depth_id)],
        )?;

        graph.add_pass(Box::new(sprite_pass), &[("color", scene_color_id)])?;

        graph.add_pass(
            Box::new(selection_mask_pass),
            &[("selection_mask", selection_mask_id), ("depth", depth_id)],
        )?;

        let outline_pass = passes::OutlinePass::new(&device, hdr_format);
        graph.add_pass(
            Box::new(outline_pass),
            &[
                ("selection_mask", selection_mask_id),
                ("color", scene_color_id),
            ],
        )?;

        let (brdf_lut_texture, brdf_lut_view) = brdf_lut::generate_brdf_lut(&device, &queue);

        let depth_pick_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("Depth Pick Shader"),
            source: wgpu::ShaderSource::Wgsl(include_str!("wgpu/shaders/depth_pick.wgsl").into()),
        });

        let depth_pick_bind_group_layout =
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                label: Some("Depth Pick Bind Group Layout"),
                entries: &[
                    wgpu::BindGroupLayoutEntry {
                        binding: 0,
                        visibility: wgpu::ShaderStages::COMPUTE,
                        ty: wgpu::BindingType::Texture {
                            sample_type: wgpu::TextureSampleType::Depth,
                            view_dimension: wgpu::TextureViewDimension::D2,
                            multisampled: false,
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 1,
                        visibility: wgpu::ShaderStages::COMPUTE,
                        ty: wgpu::BindingType::Buffer {
                            ty: wgpu::BufferBindingType::Storage { read_only: false },
                            has_dynamic_offset: false,
                            min_binding_size: None,
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 2,
                        visibility: wgpu::ShaderStages::COMPUTE,
                        ty: wgpu::BindingType::Buffer {
                            ty: wgpu::BufferBindingType::Uniform,
                            has_dynamic_offset: false,
                            min_binding_size: None,
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 3,
                        visibility: wgpu::ShaderStages::COMPUTE,
                        ty: wgpu::BindingType::Texture {
                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
                            view_dimension: wgpu::TextureViewDimension::D2,
                            multisampled: false,
                        },
                        count: None,
                    },
                ],
            });

        let depth_pick_pipeline_layout =
            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: Some("Depth Pick Pipeline Layout"),
                bind_group_layouts: &[&depth_pick_bind_group_layout],
                push_constant_ranges: &[],
            });

        let depth_pick_compute_pipeline =
            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
                label: Some("Depth Pick Compute Pipeline"),
                layout: Some(&depth_pick_pipeline_layout),
                module: &depth_pick_shader,
                entry_point: Some("main"),
                compilation_options: Default::default(),
                cache: None,
            });

        let depth_pick_buffer_size = (DEPTH_PICK_SAMPLE_SIZE * DEPTH_PICK_SAMPLE_SIZE * 8) as u64;
        let depth_pick_storage_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("Depth Pick Storage Buffer"),
            size: depth_pick_buffer_size,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
            mapped_at_creation: false,
        });

        let depth_pick_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("Depth Pick Uniform Buffer"),
            size: 16,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        let depth_pick_staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("Depth Pick Staging Buffer"),
            size: depth_pick_buffer_size,
            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
            mapped_at_creation: false,
        });

        Ok(Self {
            surface,
            device,
            queue,
            surface_config,
            surface_format,
            graph,
            depth_id,
            scene_color_id,
            compute_output_id,
            swapchain_id,
            viewport_resource_id,
            selection_mask_id,
            ui_depth_id,
            entity_id_id,
            #[cfg(feature = "egui")]
            egui_pass: Some(Box::new(egui_pass)),
            font_textures: Vec::new(),
            font_data: Vec::new(),
            font_initialized: false,
            camera_viewports: std::collections::HashMap::new(),
            _brdf_lut_texture: brdf_lut_texture,
            brdf_lut_view,
            depth_pick_compute_pipeline,
            depth_pick_bind_group_layout,
            depth_pick_storage_buffer,
            depth_pick_uniform_buffer,
            depth_pick_staging_buffer,
            depth_pick_bind_group: None,
            depth_pick_pending: false,
            depth_pick_map_complete: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
            depth_pick_center: (0, 0),
        })
    }
}

impl crate::ecs::world::Render for WgpuRenderer {
    fn render_frame(
        &mut self,
        world: &mut crate::ecs::world::World,
        ui_output: Option<crate::ecs::world::UiOutput>,
        ui_primitives: Option<crate::ecs::world::UiPrimitives>,
    ) -> Result<(), Box<dyn std::error::Error>> {
        if self.depth_pick_pending {
            let _ = self.device.poll(wgpu::PollType::Poll);

            if self
                .depth_pick_map_complete
                .load(std::sync::atomic::Ordering::Relaxed)
            {
                let buffer_slice = self.depth_pick_staging_buffer.slice(..);
                let data = buffer_slice.get_mapped_range();
                let mut depth_values = Vec::new();
                let mut entity_id_values = Vec::new();
                for chunk in data.chunks_exact(8) {
                    let depth = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
                    let entity_id = u32::from_le_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]);
                    depth_values.push(depth);
                    entity_id_values.push(entity_id);
                }
                drop(data);
                self.depth_pick_staging_buffer.unmap();

                world.resources.gpu_picking.set_depth_samples(
                    depth_values,
                    entity_id_values,
                    DEPTH_PICK_SAMPLE_SIZE,
                    DEPTH_PICK_SAMPLE_SIZE,
                    self.depth_pick_center.0,
                    self.depth_pick_center.1,
                );

                if let Some(camera_entity) = world.resources.active_camera
                    && let Some(camera) = world.get_camera(camera_entity)
                    && let Some(global_transform) = world.get_global_transform(camera_entity)
                {
                    let (viewport_width, viewport_height) = (
                        self.surface_config.width as f32,
                        self.surface_config.height as f32,
                    );
                    let aspect_ratio = viewport_width / viewport_height;
                    let view_matrix = global_transform
                        .0
                        .try_inverse()
                        .unwrap_or_else(nalgebra_glm::Mat4::identity);
                    let projection_matrix = camera.projection.matrix_with_aspect(aspect_ratio);
                    let inverse_view_proj = (projection_matrix * view_matrix)
                        .try_inverse()
                        .unwrap_or_else(nalgebra_glm::Mat4::identity);

                    world.resources.gpu_picking.compute_result(
                        &inverse_view_proj,
                        viewport_width,
                        viewport_height,
                    );
                }

                self.depth_pick_pending = false;
                self.depth_pick_map_complete
                    .store(false, std::sync::atomic::Ordering::Relaxed);
            }
        }

        let mut sprite_commands = Vec::new();
        let mut texture_load_commands = Vec::new();
        let mut hdr_skybox_commands = Vec::new();

        for cmd in world.resources.command_queue.iter() {
            match cmd {
                crate::ecs::world::WorldCommand::UploadSpriteTexture {
                    slot,
                    rgba_data,
                    width,
                    height,
                } => {
                    sprite_commands.push((*slot, rgba_data.clone(), *width, *height));
                }
                crate::ecs::world::WorldCommand::LoadTexture {
                    name,
                    rgba_data,
                    width,
                    height,
                } => {
                    texture_load_commands.push((name.clone(), rgba_data.clone(), *width, *height));
                }
                crate::ecs::world::WorldCommand::LoadHdrSkybox { hdr_data } => {
                    hdr_skybox_commands.push(hdr_data.clone());
                }
                crate::ecs::world::WorldCommand::LoadHdrSkyboxFromPath { path } => {
                    match std::fs::read(path) {
                        Ok(bytes) => {
                            tracing::info!("Successfully read HDR file: {}", path.display());
                            hdr_skybox_commands.push(bytes);
                        }
                        Err(e) => {
                            tracing::error!("Failed to read HDR file {}: {}", path.display(), e);
                        }
                    }
                }
                _ => {}
            }
        }

        if !sprite_commands.is_empty() {
            if let Some(sprite_pass) = self.graph.get_pass_mut("sprite_pass")
                && let Some(sprite_pass) =
                    (sprite_pass as &mut dyn std::any::Any).downcast_mut::<passes::SpritePass>()
            {
                for (slot, rgba_data, width, height) in sprite_commands {
                    sprite_pass.upload_texture_to_atlas(
                        &self.queue,
                        slot,
                        &rgba_data,
                        width,
                        height,
                    );
                }
            }

            world.resources.command_queue.retain(|cmd| {
                !matches!(
                    cmd,
                    crate::ecs::world::WorldCommand::UploadSpriteTexture { .. }
                )
            });
        }

        if !texture_load_commands.is_empty() {
            for (name, rgba_data, width, height) in texture_load_commands {
                if let Err(e) = texture_cache_load_from_raw_rgba(
                    &mut world.resources.texture_cache,
                    &self.device,
                    &self.queue,
                    name.clone(),
                    &rgba_data,
                    (width, height),
                ) {
                    tracing::error!("Failed to load texture: {}", e);
                    continue;
                }

                if let Some(mesh_pass) = self.graph.get_pass_mut("mesh_pass")
                    && let Some(mesh_pass) =
                        (mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
                    && let Some(texture_entry) =
                        registry_entry_by_name(&world.resources.texture_cache.registry, &name)
                {
                    mesh_pass.register_texture(
                        name.clone(),
                        texture_entry.view.clone(),
                        texture_entry.sampler.clone(),
                    );
                }

                if let Some(skinned_mesh_pass) = self.graph.get_pass_mut("skinned_mesh_pass")
                    && let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
                        .downcast_mut::<passes::geometry::SkinnedMeshPass>(
                    )
                    && let Some(texture_entry) =
                        registry_entry_by_name(&world.resources.texture_cache.registry, &name)
                {
                    skinned_mesh_pass.register_texture(
                        name.clone(),
                        texture_entry.view.clone(),
                        texture_entry.sampler.clone(),
                    );
                }

                if let Some(decal_pass) = self.graph.get_pass_mut("decal_pass")
                    && let Some(decal_pass) =
                        (decal_pass as &mut dyn std::any::Any).downcast_mut::<passes::DecalPass>()
                    && let Some(texture_entry) =
                        registry_entry_by_name(&world.resources.texture_cache.registry, &name)
                {
                    decal_pass.register_texture(
                        name,
                        texture_entry.view.clone(),
                        texture_entry.sampler.clone(),
                    );
                }
            }

            world
                .resources
                .command_queue
                .retain(|cmd| !matches!(cmd, crate::ecs::world::WorldCommand::LoadTexture { .. }));
        }

        if !hdr_skybox_commands.is_empty() {
            let mut ibl_views = None;

            if let Some(sky_pass) = self.graph.get_pass_mut("sky_pass")
                && let Some(sky_pass) =
                    (sky_pass as &mut dyn std::any::Any).downcast_mut::<passes::geometry::SkyPass>()
            {
                for hdr_data in hdr_skybox_commands {
                    ibl_views =
                        Some(sky_pass.load_hdr_skybox(&self.device, &self.queue, &hdr_data));
                }
            }

            if let Some((irradiance_view, prefiltered_view)) = ibl_views {
                if let Some(mesh_pass) = self.graph.get_pass_mut("mesh_pass")
                    && let Some(mesh_pass) =
                        (mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
                {
                    mesh_pass.update_ibl_textures(
                        self.brdf_lut_view.clone(),
                        irradiance_view.clone(),
                        prefiltered_view.clone(),
                    );
                }

                if let Some(skinned_mesh_pass) = self.graph.get_pass_mut("skinned_mesh_pass")
                    && let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
                        .downcast_mut::<passes::geometry::SkinnedMeshPass>(
                    )
                {
                    skinned_mesh_pass.set_ibl_textures(
                        self.brdf_lut_view.clone(),
                        irradiance_view,
                        prefiltered_view,
                    );
                }
            }

            world.resources.command_queue.retain(|cmd| {
                !matches!(
                    cmd,
                    crate::ecs::world::WorldCommand::LoadHdrSkybox { .. }
                        | crate::ecs::world::WorldCommand::LoadHdrSkyboxFromPath { .. }
                )
            });
        }

        if let Some(lines_pass) = self.graph.get_pass_mut("lines_pass")
            && let Some(lines_pass) =
                (lines_pass as &mut dyn std::any::Any).downcast_mut::<passes::LinesPass>()
        {
            passes::sync_lines_data(lines_pass, &self.device, &self.queue, world);
            passes::sync_bounding_volume_data(
                lines_pass,
                &self.device,
                &self.queue,
                world,
                world.resources.graphics.show_bounding_volumes,
                world.resources.graphics.show_selected_bounding_volume,
                world.resources.graphics.bounding_volume_selected_entity,
            );
            passes::sync_normal_data(
                lines_pass,
                &self.device,
                &self.queue,
                world,
                world.resources.graphics.show_normals,
                world.resources.graphics.normal_line_length,
                world.resources.graphics.normal_line_color,
            );
        }

        #[cfg(feature = "egui")]
        if let Some(egui::FullOutput {
            ref textures_delta,
            pixels_per_point,
            ..
        }) = ui_output
        {
            if let Some(egui_pass) = self.graph.get_pass_mut("egui_pass")
                && let Some(egui_pass) =
                    (egui_pass as &mut dyn std::any::Any).downcast_mut::<passes::EguiPass>()
            {
                egui_pass.update_textures(&self.device, &self.queue, textures_delta);
            }

            if let Some(ref paint_jobs) = ui_primitives {
                let screen_descriptor = egui_wgpu::ScreenDescriptor {
                    size_in_pixels: [self.surface_config.width, self.surface_config.height],
                    pixels_per_point,
                };
                if let Some(egui_pass) = self.graph.get_pass_mut("egui_pass")
                    && let Some(egui_pass) =
                        (egui_pass as &mut dyn std::any::Any).downcast_mut::<passes::EguiPass>()
                {
                    egui_pass.set_screen_descriptor(screen_descriptor);
                    egui_pass.set_paint_jobs(paint_jobs.clone());
                }
            }
        }
        #[cfg(not(feature = "egui"))]
        let _ = (ui_output, ui_primitives);

        let point_shadow_view = self
            .graph
            .get_pass_mut("shadow_depth_pass")
            .and_then(|pass| {
                (pass as &dyn std::any::Any)
                    .downcast_ref::<passes::ShadowDepthPass>()
                    .map(|shadow_pass| shadow_pass.point_light_cubemap_view().clone())
            });

        if let Some(view) = point_shadow_view {
            if let Some(mesh_pass) = self.graph.get_pass_mut("mesh_pass")
                && let Some(mesh_pass) =
                    (mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
            {
                mesh_pass.update_point_shadow_cubemap(view.clone());
            }
            if let Some(skinned_mesh_pass) = self.graph.get_pass_mut("skinned_mesh_pass")
                && let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
                    .downcast_mut::<passes::SkinnedMeshPass>()
            {
                skinned_mesh_pass.update_point_shadow_cubemap(view);
            }
        }

        let surface_texture = match self.surface.get_current_texture() {
            Ok(frame) => frame,
            Err(wgpu::SurfaceError::Outdated) => {
                self.surface.configure(&self.device, &self.surface_config);
                self.surface.get_current_texture()?
            }
            Err(wgpu::SurfaceError::Lost) => {
                self.surface.configure(&self.device, &self.surface_config);
                self.surface.get_current_texture()?
            }
            Err(wgpu::SurfaceError::Timeout) => {
                return Ok(());
            }
            Err(e) => return Err(Box::new(e)),
        };

        let surface_texture_view =
            surface_texture
                .texture
                .create_view(&wgpu::TextureViewDescriptor {
                    label: wgpu::Label::default(),
                    aspect: wgpu::TextureAspect::default(),
                    format: Some(self.surface_format),
                    dimension: None,
                    base_mip_level: 0,
                    mip_level_count: None,
                    base_array_layer: 0,
                    array_layer_count: None,
                    usage: None,
                });

        self.graph.set_external_texture(
            self.swapchain_id,
            surface_texture_view,
            self.surface_config.width,
            self.surface_config.height,
        );

        let required_cameras = world.resources.user_interface.required_cameras.clone();
        self.cleanup_unused_camera_viewports(&required_cameras);

        #[cfg(feature = "egui")]
        world.resources.user_interface.viewport_textures.clear();

        let has_viewports = !required_cameras.is_empty();
        let _ = self
            .graph
            .set_pass_enabled("viewport_blit_pass", has_viewports);

        let original_active_camera = world.resources.active_camera;

        for camera_entity in &required_cameras {
            #[cfg(feature = "egui")]
            self.ensure_camera_viewport(
                *camera_entity,
                self.surface_config.width,
                self.surface_config.height,
            );

            if let Some(viewport) = self.camera_viewports.get(camera_entity) {
                self.graph.set_external_texture(
                    self.viewport_resource_id,
                    viewport.view.clone(),
                    self.surface_config.width,
                    self.surface_config.height,
                );

                world.resources.active_camera = Some(*camera_entity);

                let command_buffers = self.graph.execute(&self.device, &self.queue, world)?;
                self.queue.submit(command_buffers);

                #[cfg(feature = "egui")]
                world
                    .resources
                    .user_interface
                    .viewport_textures
                    .push(viewport.texture_id);
            }
        }

        world.resources.active_camera = original_active_camera;

        if required_cameras.is_empty() {
            let command_buffers = self.graph.execute(&self.device, &self.queue, world)?;
            self.queue.submit(command_buffers);
        }

        if !self.depth_pick_pending
            && let Some(request) = world.resources.gpu_picking.take_pending_request()
            && let Some(depth_texture_view) = self.graph.get_texture_view(self.depth_id)
            && let Some(entity_id_texture_view) = self.graph.get_texture_view(self.entity_id_id)
        {
            let uniform_data: [u32; 4] = [
                request.screen_x,
                request.screen_y,
                DEPTH_PICK_SAMPLE_SIZE,
                0,
            ];
            self.queue.write_buffer(
                &self.depth_pick_uniform_buffer,
                0,
                bytemuck::cast_slice(&uniform_data),
            );

            let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
                label: Some("Depth Pick Bind Group"),
                layout: &self.depth_pick_bind_group_layout,
                entries: &[
                    wgpu::BindGroupEntry {
                        binding: 0,
                        resource: wgpu::BindingResource::TextureView(depth_texture_view),
                    },
                    wgpu::BindGroupEntry {
                        binding: 1,
                        resource: self.depth_pick_storage_buffer.as_entire_binding(),
                    },
                    wgpu::BindGroupEntry {
                        binding: 2,
                        resource: self.depth_pick_uniform_buffer.as_entire_binding(),
                    },
                    wgpu::BindGroupEntry {
                        binding: 3,
                        resource: wgpu::BindingResource::TextureView(entity_id_texture_view),
                    },
                ],
            });

            let mut encoder = self
                .device
                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                    label: Some("Depth Pick Encoder"),
                });

            {
                let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
                    label: Some("Depth Pick Pass"),
                    timestamp_writes: None,
                });

                compute_pass.set_pipeline(&self.depth_pick_compute_pipeline);
                compute_pass.set_bind_group(0, &bind_group, &[]);
                compute_pass.dispatch_workgroups(DEPTH_PICK_SAMPLE_SIZE, DEPTH_PICK_SAMPLE_SIZE, 1);
            }

            encoder.copy_buffer_to_buffer(
                &self.depth_pick_storage_buffer,
                0,
                &self.depth_pick_staging_buffer,
                0,
                (DEPTH_PICK_SAMPLE_SIZE * DEPTH_PICK_SAMPLE_SIZE * 8) as u64,
            );

            self.queue.submit(std::iter::once(encoder.finish()));
            self.depth_pick_bind_group = Some(bind_group);
            self.depth_pick_pending = true;
            self.depth_pick_center = (request.screen_x, request.screen_y);

            let map_complete = self.depth_pick_map_complete.clone();
            self.depth_pick_staging_buffer
                .slice(..)
                .map_async(wgpu::MapMode::Read, move |_| {
                    map_complete.store(true, std::sync::atomic::Ordering::Relaxed);
                });
        }

        surface_texture.present();

        Ok(())
    }

    fn resize_surface(
        &mut self,
        width: u32,
        height: u32,
    ) -> Result<(), Box<dyn std::error::Error>> {
        if width == 0 || height == 0 {
            return Ok(());
        }

        tracing::info!("Resizing wgpu surface to {width}x{height}");
        self.surface_config.width = width;
        self.surface_config.height = height;
        self.surface.configure(&self.device, &self.surface_config);

        self.graph.resize_transient_resource(
            &self.device,
            self.depth_id,
            width.max(1),
            height.max(1),
        )?;

        self.graph.resize_transient_resource(
            &self.device,
            self.scene_color_id,
            width.max(1),
            height.max(1),
        )?;

        self.graph.resize_transient_resource(
            &self.device,
            self.compute_output_id,
            width.max(1),
            height.max(1),
        )?;

        self.graph.resize_transient_resource(
            &self.device,
            self.selection_mask_id,
            width.max(1),
            height.max(1),
        )?;

        self.graph.resize_transient_resource(
            &self.device,
            self.entity_id_id,
            width.max(1),
            height.max(1),
        )?;

        self.graph.resize_transient_resource(
            &self.device,
            self.ui_depth_id,
            width.max(1),
            height.max(1),
        )?;

        if let Some(mesh_pass) = self.graph.get_pass_mut("mesh_pass")
            && let Some(mesh_pass) =
                (mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
        {
            mesh_pass.resize(&self.device, width.max(1), height.max(1));
        }

        if let Some(bloom_pass) = self.graph.get_pass_mut("bloom_pass")
            && let Some(bloom_pass) =
                (bloom_pass as &mut dyn std::any::Any).downcast_mut::<passes::BloomPass>()
        {
            bloom_pass.resize(&self.device, &self.queue, width.max(1), height.max(1));
        }

        if let Some(outline_pass) = self.graph.get_pass_mut("outline_pass")
            && let Some(outline_pass) =
                (outline_pass as &mut dyn std::any::Any).downcast_mut::<passes::OutlinePass>()
        {
            outline_pass.resize(width.max(1), height.max(1));
        }

        Ok(())
    }

    fn surface_size(&self) -> (u32, u32) {
        (self.surface_config.width, self.surface_config.height)
    }

    fn configure_with_state(
        &mut self,
        state: &mut dyn crate::run::State,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let font_atlas = font_atlas::generate_font_atlas_sdf(
            &self.device,
            &self.queue,
            font_atlas::DEFAULT_FONT_DATA,
            32.0,
        )?;

        self.font_textures
            .push((font_atlas.texture, font_atlas.texture_view));
        let texture_index = (self.font_textures.len() - 1) as u32;

        let font_data = crate::ecs::text::resources::FontAtlasData {
            texture_index,
            glyphs: font_atlas.glyphs,
            width: font_atlas.width,
            height: font_atlas.height,
            font_size: font_atlas.font_size,
            sdf_range: font_atlas.sdf_range,
        };

        self.font_data.push(font_data);

        let mut text_pass = passes::TextPass::new(
            &self.device,
            wgpu::TextureFormat::Rgba16Float,
            wgpu::TextureFormat::Depth32Float,
        );

        text_pass.update_texture_bind_groups(&self.device, &self.font_textures);

        self.graph.add_pass(
            Box::new(text_pass),
            &[("color", self.scene_color_id), ("depth", self.depth_id)],
        )?;

        state.configure_render_graph(
            &mut self.graph,
            &self.device,
            self.surface_format,
            crate::run::RenderResources {
                scene_color: self.scene_color_id,
                depth: self.depth_id,
                compute_output: self.compute_output_id,
                swapchain: self.swapchain_id,
            },
        );

        let viewport_blit_pass = passes::BlitPass::new(&self.device, self.surface_format)
            .with_name("viewport_blit_pass");
        self.graph.add_pass(
            Box::new(viewport_blit_pass),
            &[
                ("input", self.compute_output_id),
                ("output", self.viewport_resource_id),
            ],
        )?;

        let ui_rect_pass = passes::UiRectPass::new(&self.device, self.surface_format);
        self.graph.add_pass(
            Box::new(ui_rect_pass),
            &[("color", self.swapchain_id), ("depth", self.ui_depth_id)],
        )?;

        let mut hud_pass = passes::HudPass::new(&self.device, self.surface_format);

        hud_pass.update_texture_bind_groups(&self.device, &self.font_textures);

        self.graph.add_pass(
            Box::new(hud_pass),
            &[("color", self.swapchain_id), ("depth", self.ui_depth_id)],
        )?;

        #[cfg(feature = "egui")]
        if let Some(egui_pass) = self.egui_pass.take() {
            self.graph
                .add_pass(egui_pass, &[("swapchain", self.swapchain_id)])?;
        }

        self.graph.compile()?;
        Ok(())
    }

    fn initialize_fonts(&mut self, world: &mut crate::ecs::world::World) {
        if !self.font_initialized {
            for font_data in self.font_data.drain(..) {
                let font_index = world.resources.text_cache.font_manager.add_font(font_data);
                if world.resources.text_cache.font_manager.default_font == 0 {
                    world.resources.text_cache.font_manager.default_font = font_index;
                }
                tracing::info!("Initialized font at index {}", font_index);
            }
            self.font_initialized = true;
        }
    }

    fn update_with_state(
        &mut self,
        state: &mut dyn crate::run::State,
        world: &mut crate::ecs::world::World,
    ) -> Result<(), Box<dyn std::error::Error>> {
        self.initialize_fonts(world);

        for pending_load in world.resources.pending_font_loads.drain(..) {
            let font_atlas = match font_atlas::generate_font_atlas_sdf(
                &self.device,
                &self.queue,
                &pending_load.font_data,
                pending_load.font_size,
            ) {
                Ok(atlas) => atlas,
                Err(e) => {
                    tracing::error!("Failed to load font: {}", e);
                    continue;
                }
            };

            self.font_textures
                .push((font_atlas.texture, font_atlas.texture_view));
            let texture_index = (self.font_textures.len() - 1) as u32;

            let font_data = crate::ecs::text::resources::FontAtlasData {
                texture_index,
                glyphs: font_atlas.glyphs,
                width: font_atlas.width,
                height: font_atlas.height,
                font_size: font_atlas.font_size,
                sdf_range: font_atlas.sdf_range,
            };

            let font_index = world.resources.text_cache.font_manager.add_font(font_data);

            if let Some(text_pass) = self.graph.get_pass_mut("text_pass")
                && let Some(text_pass) =
                    (text_pass as &mut dyn std::any::Any).downcast_mut::<passes::TextPass>()
            {
                text_pass.update_texture_bind_groups(&self.device, &self.font_textures);
            }

            if let Some(hud_pass) = self.graph.get_pass_mut("hud_pass")
                && let Some(hud_pass) =
                    (hud_pass as &mut dyn std::any::Any).downcast_mut::<passes::HudPass>()
            {
                hud_pass.update_texture_bind_groups(&self.device, &self.font_textures);
            }

            tracing::info!("Loaded custom font at index {}", font_index);
        }

        state.update_render_graph(&mut self.graph, world);
        Ok(())
    }
}

pub async fn create_wgpu_renderer<W>(
    window_handle: W,
    initial_width: u32,
    initial_height: u32,
) -> Result<WgpuRenderer, Box<dyn std::error::Error>>
where
    W: Into<wgpu::SurfaceTarget<'static>>,
{
    WgpuRenderer::new_async(window_handle, initial_width, initial_height).await
}
