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;

pub struct CameraViewport {
    pub texture: wgpu::Texture,
    pub view: wgpu::TextureView,
    pub texture_id: egui::TextureId,
    pub size: (u32, u32),
}

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,
    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>,
}

impl WgpuRenderer {
    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;
        }

        let (device, queue) = adapter
            .request_device(&wgpu::DeviceDescriptor {
                label: Some("WGPU Device"),
                memory_hints: wgpu::MemoryHints::default(),
                required_features,
                required_limits: wgpu::Limits::default().using_resolution(adapter.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 lines_pass = passes::LinesPass::new(&device, hdr_format);

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

        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))
            .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(wgpu::TextureFormat::Rgba8Unorm)
            .size(initial_width.max(1), initial_height.max(1))
            .usage(
                wgpu::TextureUsages::STORAGE_BINDING
                    | 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") {
            2048
        } 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();

        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)],
        )?;

        graph.add_pass(
            Box::new(mesh_pass),
            &[
                ("color", scene_color_id),
                ("depth", depth_id),
                ("shadow_depth", shadow_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)])?;

        Ok(Self {
            surface,
            device,
            queue,
            surface_config,
            surface_format,
            graph,
            depth_id,
            scene_color_id,
            compute_output_id,
            swapchain_id,
            viewport_resource_id,
            egui_pass: Some(Box::new(egui_pass)),
            font_textures: Vec::new(),
            font_data: Vec::new(),
            font_initialized: false,
            camera_viewports: std::collections::HashMap::new(),
        })
    }
}

impl crate::ecs::world::Render for WgpuRenderer {
    fn render_frame(
        &mut self,
        world: &mut crate::ecs::world::World,
        ui_output: Option<egui::FullOutput>,
        ui_primitives: Option<Vec<egui::ClippedPrimitive>>,
    ) -> Result<(), Box<dyn std::error::Error>> {
        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) => {
                            log::info!("Successfully read HDR file: {}", path.display());
                            hdr_skybox_commands.push(bytes);
                        }
                        Err(e) => {
                            log::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) = world.resources.texture_cache.load_texture_from_raw_rgba(
                    &self.device,
                    &self.queue,
                    name.clone(),
                    &rgba_data,
                    width,
                    height,
                ) {
                    log::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) = world.resources.texture_cache.get(&name)
                {
                    mesh_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() {
            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 {
                    sky_pass.load_hdr_skybox(&self.device, &self.queue, &hdr_data);
                }
            }

            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);
        }

        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());
                }
            }
        }

        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);

        world.resources.user_interface.viewport_textures.clear();

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

        let original_active_camera = world.resources.active_camera;

        for camera_entity in &required_cameras {
            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);

                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);
        }

        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(());
        }

        log::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),
        )?;

        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));
        }

        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 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)])?;

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

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

    fn update_with_state(
        &mut self,
        state: &mut dyn crate::run::State,
        world: &mut crate::ecs::world::World,
    ) -> Result<(), Box<dyn std::error::Error>> {
        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;
                }
                log::info!("Initialized font at index {}", font_index);
            }
            self.font_initialized = true;
        }

        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) => {
                    log::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);
            }

            log::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
}
