use crate::errors::AleaticoResult;
use crate::renderer::ctx::Gpu;
use crate::renderer::render_item::RenderItem;
use crate::renderer::render_pass::MainPass;
use crate::renderer::resources::mesh::{Mesh, MeshId};
use crate::renderer::resources::pipelines::{Pipeline, PipelineDescriptor, PipelineId};
use crate::renderer::resources::texture::TextureId;
use crate::renderer::resources::{ResourceStore, texture};
use crate::renderer::surface::Surface;
use std::sync::Arc;
use winit::window::Window;
use crate::renderer::camera::Camera;
use crate::renderer::resources::material::{Material, MaterialDescriptor, MaterialId};
use crate::renderer::vertex::DescribableVertex;

/// GPU context
pub mod ctx;
/// Render item, not owning anything but containing IDs required for rendering
pub mod render_item;
/// Render passes
pub mod render_pass;
/// All the render resources (incl. but not limited to pipelines, textures, meshes, materials)
pub mod resources;
/// Rendering surface
pub mod surface;
/// Vertices
pub mod vertex;
/// Implements a 3D camera
pub mod camera;

/// Rendering statistics
#[derive(Clone, Copy, Debug, Default)]
pub struct RenderStats {
    /// Amount of triangles rendered in a frame
    pub triangles: usize,
}

/// Renderer owning a GPU context and the main surface
pub struct Renderer {
    gpu: Gpu,
    surface: Surface,
    settings: RenderSettings,
    resources: ResourceStore,
    stats: RenderStats,
    camera: Camera,
}

/// Renderer settings
#[derive(Debug, Clone, Copy)]
pub struct RenderSettings {
    /// Color to use for surface clearing
    pub clear_color: wgpu::Color,
}

impl Default for RenderSettings {
    fn default() -> Self {
        Self {
            clear_color: wgpu::Color {
                r: 0.0,
                g: 0.0,
                b: 0.0,
                a: 1.0,
            },
        }
    }
}

impl Renderer {
    /// Create a new instance of [`Renderer`]
    pub async fn new(window: Arc<Window>) -> AleaticoResult<Self> {
        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
            #[cfg(not(target_arch = "wasm32"))]
            backends: wgpu::Backends::PRIMARY,
            #[cfg(target_arch = "wasm32")]
            backends: wgpu::Backends::GL,
            flags: Default::default(),
            memory_budget_thresholds: Default::default(),
            backend_options: Default::default(),
            display: None,
        });
        let raw_surface = instance.create_surface(window.clone())?;
        let mut adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                power_preference: wgpu::PowerPreference::default(),
                compatible_surface: Some(&raw_surface),
                force_fallback_adapter: false,
            })
            .await?;
        let surface = Surface::new(window, raw_surface, &mut adapter)?;
        let (width, height) = (surface.config.width as f32, surface.config.height as f32);

        let mut gpu = Gpu::new(instance, adapter).await?;
        let camera = Camera::new(&mut gpu, width, height);

        let resources = ResourceStore::new(&mut gpu, surface.config.format, camera.bind_group_layout())?;
        Ok(Self {
            gpu,
            surface,
            settings: Default::default(),
            resources,
            stats: Default::default(),
            camera,
        })
    }

    /// Render. This function clears the render item queue after finishing its job, so you need to submit
    /// render items in your controller's `render` function
    pub fn render(&mut self) -> AleaticoResult<()> {
        let Some(frame) = self.surface.acquire_frame(self.gpu.device())? else {
            return Ok(());
        };
        let mut encoder = self.gpu.create_encoder(Some("Render Pass Encoder"));
        let color_attachments = wgpu::RenderPassColorAttachment {
            view: &frame.view(),
            resolve_target: None,
            depth_slice: None,
            ops: wgpu::Operations {
                load: wgpu::LoadOp::Clear(self.settings.clear_color),
                store: wgpu::StoreOp::Store,
            },
        };

        {
            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("Render Pass"),
                color_attachments: &[Some(color_attachments)],
                depth_stencil_attachment: None,
                occlusion_query_set: None,
                timestamp_writes: None,
                multiview_mask: None,
            });

            MainPass::render(&mut render_pass, &self.camera, &self.resources, &mut self.stats)?;
        }

        self.gpu.submit(std::iter::once(encoder.finish()));
        frame.present();
        self.resources.render_item_store.clear();
        Ok(())
    }

    /// Handle renderer window resize
    pub fn resize(&mut self, width: u32, height: u32) {
        self.surface.resize(self.gpu.device(), width, height);
    }

    /// Get renderer stats
    pub fn stats(&self) -> RenderStats {
        self.stats
    }

    /// Submit an item for rendering
    pub fn submit(&mut self, render_item: RenderItem) {
        self.resources.render_item_store.push(render_item);
    }

    /// Create a pipeline
    pub fn create_pipeline<'a, V: DescribableVertex<'a>>(&mut self, shader: &'a str) -> AleaticoResult<PipelineId> {
        let pipeline = Pipeline::new(
            self.gpu.device(),
            PipelineDescriptor::from_wgsl::<V>(self.surface.config.format, shader),
            self.camera.bind_group_layout(),
        )?;
        Ok(self.resources.pipeline_store.insert(pipeline))
    }

    /// Create a non-indexed mesh
    pub fn create_non_indexed_mesh<T: bytemuck::Pod + bytemuck::Zeroable>(
        &mut self,
        vertices: &[T],
    ) -> AleaticoResult<MeshId> {
        let mesh = Mesh::non_indexed(&self.gpu.device(), vertices);
        Ok(self.resources.mesh_store.insert(mesh))
    }


    /// Create an indexed mesh
    pub fn create_indexed_mesh<T: bytemuck::Pod + bytemuck::Zeroable>(
        &mut self,
        vertices: &[T],
        indices: &[u16],
    ) -> AleaticoResult<MeshId> {
        let mesh = Mesh::indexed(&self.gpu.device(), vertices, indices);
        Ok(self.resources.mesh_store.insert(mesh))
    }

    /// Create a texture from bytes
    pub fn create_texture(&mut self, bytes: &[u8]) -> AleaticoResult<TextureId> {
        let texture = texture::Texture::from_bytes(&mut self.gpu, bytes)?;
        Ok(self.resources.texture_store.insert(texture))
    }

    /// Create a material
    pub fn create_material(&mut self, descriptor: MaterialDescriptor) -> AleaticoResult<MaterialId> {
        let material = Material::new(self.resources.builtin_pipelines().textured, descriptor)?;
        Ok(self.resources.material_store.insert(material))
    }
}
