use crate::ecs::world::World;

#[cfg(feature = "egui")]
pub type UiOutput = egui::FullOutput;
#[cfg(not(feature = "egui"))]
pub type UiOutput = ();

#[cfg(feature = "egui")]
pub type UiPrimitives = Vec<egui::ClippedPrimitive>;
#[cfg(not(feature = "egui"))]
pub type UiPrimitives = ();

pub trait Render {
    fn render_frame(
        &mut self,
        _world: &mut World,
        ui_output: Option<UiOutput>,
        ui_primitives: Option<UiPrimitives>,
    ) -> Result<(), Box<dyn std::error::Error>>;

    fn resize_surface(&mut self, width: u32, height: u32)
    -> Result<(), Box<dyn std::error::Error>>;

    fn surface_size(&self) -> (u32, u32);

    fn configure_with_state(
        &mut self,
        _state: &mut dyn crate::run::State,
    ) -> Result<(), Box<dyn std::error::Error>> {
        Ok(())
    }

    fn update_with_state(
        &mut self,
        _state: &mut dyn crate::run::State,
        _world: &mut World,
    ) -> Result<(), Box<dyn std::error::Error>> {
        Ok(())
    }

    fn initialize_fonts(&mut self, _world: &mut World) {}

    fn copy_fonts_to_world(&self, _world: &mut World) {}

    fn render_world_to_texture(
        &mut self,
        _world: &mut World,
        _target_texture: Option<&wgpu::Texture>,
        _target_view: &wgpu::TextureView,
        _width: u32,
        _height: u32,
    ) -> Result<(), Box<dyn std::error::Error>> {
        Ok(())
    }

    fn create_secondary_surface(
        &mut self,
        _id: usize,
        _window: std::sync::Arc<winit::window::Window>,
        _width: u32,
        _height: u32,
    ) -> Result<(), Box<dyn std::error::Error>> {
        Ok(())
    }

    fn remove_secondary_surface(&mut self, _id: usize) {}

    fn resize_secondary_surface(
        &mut self,
        _id: usize,
        _width: u32,
        _height: u32,
    ) -> Result<(), Box<dyn std::error::Error>> {
        Ok(())
    }

    fn render_world_to_secondary_surface(
        &mut self,
        _id: usize,
        _world: &mut World,
    ) -> Result<(), Box<dyn std::error::Error>> {
        Ok(())
    }

    fn render_retained_ui_to_secondary_surface(
        &mut self,
        _id: usize,
        _world: &mut World,
    ) -> Result<(), Box<dyn std::error::Error>> {
        Ok(())
    }

    fn secondary_surface_size(&self, _id: usize) -> Option<(u32, u32)> {
        None
    }

    fn initialize_secondary_egui(&mut self, _id: usize) {}

    #[cfg(feature = "egui")]
    fn register_secondary_egui_texture(
        &mut self,
        _id: usize,
        _view: &wgpu::TextureView,
    ) -> Option<egui::TextureId> {
        None
    }

    #[cfg(feature = "egui")]
    fn register_camera_viewports_for_secondary(
        &mut self,
        _window_id: usize,
        _cameras: &[freecs::Entity],
    ) -> Vec<egui::TextureId> {
        vec![]
    }

    fn render_egui_to_secondary_surface(
        &mut self,
        _id: usize,
        _ui_output: Option<UiOutput>,
        _ui_primitives: Option<UiPrimitives>,
    ) -> Result<(), Box<dyn std::error::Error>> {
        Ok(())
    }

    fn device(&self) -> &wgpu::Device;
    fn queue(&self) -> &wgpu::Queue;
    fn surface_format(&self) -> wgpu::TextureFormat;

    #[cfg(feature = "egui")]
    fn register_egui_texture(&mut self, _view: &wgpu::TextureView) -> Option<egui::TextureId> {
        None
    }

    fn register_render_texture(&mut self, _name: &str, _view: wgpu::TextureView) {}

    #[cfg(not(target_arch = "wasm32"))]
    fn read_texture_to_rgba(
        &self,
        texture: &wgpu::Texture,
        width: u32,
        height: u32,
    ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        let device = self.device();
        let queue = self.queue();

        let bytes_per_pixel = 4u32;
        let unpadded_bytes_per_row = width * bytes_per_pixel;
        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
        let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
        let buffer_size = (padded_bytes_per_row * height) as u64;

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

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

        encoder.copy_texture_to_buffer(
            wgpu::TexelCopyTextureInfo {
                texture,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            wgpu::TexelCopyBufferInfo {
                buffer: &staging_buffer,
                layout: wgpu::TexelCopyBufferLayout {
                    offset: 0,
                    bytes_per_row: Some(padded_bytes_per_row),
                    rows_per_image: Some(height),
                },
            },
            wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
        );

        queue.submit(std::iter::once(encoder.finish()));

        let buffer_slice = staging_buffer.slice(..);
        let map_complete = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
        let map_complete_clone = map_complete.clone();
        buffer_slice.map_async(wgpu::MapMode::Read, move |_| {
            map_complete_clone.store(true, std::sync::atomic::Ordering::Release);
        });
        while !map_complete.load(std::sync::atomic::Ordering::Acquire) {
            let _ = device.poll(wgpu::PollType::Poll);
        }

        let data = buffer_slice.get_mapped_range();

        let mut pixels = Vec::with_capacity((width * height * bytes_per_pixel) as usize);
        for row in 0..height {
            let start = (row * padded_bytes_per_row) as usize;
            let end = start + (unpadded_bytes_per_row) as usize;
            pixels.extend_from_slice(&data[start..end]);
        }
        drop(data);
        staging_buffer.unmap();

        Ok(pixels)
    }

    #[cfg(all(not(target_arch = "wasm32"), feature = "screenshot"))]
    fn save_texture_to_file(
        &self,
        texture: &wgpu::Texture,
        width: u32,
        height: u32,
        path: &std::path::Path,
    ) {
        let pixels = match self.read_texture_to_rgba(texture, width, height) {
            Ok(pixels) => pixels,
            Err(error) => {
                tracing::error!("Failed to read texture: {}", error);
                return;
            }
        };

        let path = path.to_owned();
        std::thread::spawn(
            move || match image::RgbaImage::from_raw(width, height, pixels) {
                Some(image) => {
                    if let Err(error) = image.save(&path) {
                        tracing::error!("Failed to save texture: {}", error);
                    } else {
                        tracing::info!("Saved texture to: {}", path.display());
                    }
                }
                None => {
                    tracing::error!("Failed to create image from texture data");
                }
            },
        );
    }
}
