// BEWARE OF THE PIPELINE:
// he/him -> he/they -> she/they -> she/her

use crate::define_store;
use crate::errors::AleaticoResult;
use crate::renderer::vertex::DescribableVertex;
use wgpu::{BindGroupLayout, Device, ShaderSource};

define_store!(Pipeline);

/// Struct representing a rendering pipeline
pub struct Pipeline {
    /// Inner [`wgpu`] render pipeline
    pub(crate) render_pipeline: wgpu::RenderPipeline,
}

/// Descriptor of a [`Pipeline`].
pub struct PipelineDescriptor<'a> {
    /// Pipeline label
    pub label: Option<&'a str>,
    /// Shader data
    pub shader: String,
    /// Color format of the render target
    pub format: wgpu::TextureFormat,
    /// Vertex buffer layouts
    pub vertex_layouts: Vec<wgpu::VertexBufferLayout<'a>>,
    /// Entry point name for the vertex shader stage
    pub vertex_entry_point: &'a str,
    /// Entry point name for the fragment shader stage
    pub fragment_entry_point: &'a str,
    /// Primitive type the input mesh is composed of
    pub topology: wgpu::PrimitiveTopology,
    /// Vertex winding order which classifies the "front" face of a triangle
    pub front_face: wgpu::FrontFace,
    /// Face of a vertex
    pub cull_mode: Option<wgpu::Face>,
    /// Type of drawing mode for polygons
    pub polygon_mode: wgpu::PolygonMode,
    /// Blend state
    pub blend: Option<wgpu::BlendState>,
    /// Color write mask
    pub write_mask: wgpu::ColorWrites,
    /// Optional depth stencil state
    pub depth_stencil: Option<wgpu::DepthStencilState>,
    /// Multisampling configuration for the pipeline
    pub multisample: wgpu::MultisampleState,
}

impl<'a> PipelineDescriptor<'a> {
    /// Create a default triangle-list pipeline for the generic vertex type
    pub fn from_wgsl<V: DescribableVertex<'a>>(format: wgpu::TextureFormat, shader: &'a str) -> Self {
        Self {
            label: Some("Render Pipeline"),
            shader: shader.to_string(),

            format,

            vertex_layouts: vec![V::desc()],

            vertex_entry_point: "vs_main",
            fragment_entry_point: "fs_main",

            topology: wgpu::PrimitiveTopology::TriangleList,
            front_face: wgpu::FrontFace::Ccw,
            cull_mode: Some(wgpu::Face::Back),
            polygon_mode: wgpu::PolygonMode::Fill,

            blend: Some(wgpu::BlendState::REPLACE),
            write_mask: wgpu::ColorWrites::ALL,

            depth_stencil: None,
            multisample: wgpu::MultisampleState {
                count: 1,
                mask: !0,
                alpha_to_coverage_enabled: false,
            },
        }
    }

    /// Set label
    pub fn with_label(mut self, label: &'a str) -> Self {
        self.label = Some(label);
        self
    }

    /// Set vertex layouts
    pub fn with_vertex_layouts(
        mut self,
        vertex_layouts: Vec<wgpu::VertexBufferLayout<'a>>,
    ) -> Self {
        self.vertex_layouts = vertex_layouts;
        self
    }

    /// Set entry points
    pub fn with_entry_points(
        mut self,
        vertex_entry_point: &'a str,
        fragment_entry_point: &'a str,
    ) -> Self {
        self.vertex_entry_point = vertex_entry_point;
        self.fragment_entry_point = fragment_entry_point;
        self
    }

    /// Set topology
    pub fn with_topology(mut self, topology: wgpu::PrimitiveTopology) -> Self {
        self.topology = topology;
        self
    }

    /// Set culling mode
    pub fn with_cull_mode(mut self, cull_mode: Option<wgpu::Face>) -> Self {
        self.cull_mode = cull_mode;
        self
    }

    /// Set blend state
    pub fn with_blend(mut self, blend: Option<wgpu::BlendState>) -> Self {
        self.blend = blend;
        self
    }

    /// Set depth stencil
    pub fn with_depth_stencil(mut self, depth_stencil: Option<wgpu::DepthStencilState>) -> Self {
        self.depth_stencil = depth_stencil;
        self
    }
}

impl Pipeline {
    /// Create a new pipeline from a [`PipelineDescriptor`]
    pub fn new(device: &Device, descriptor: PipelineDescriptor<'_>, camera_layout: &BindGroupLayout) -> AleaticoResult<Self> {
        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: descriptor.label,
            source: ShaderSource::Wgsl(descriptor.shader.as_str().into()),
        });
        let texture_bind_group_layout =
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                entries: &[
                    wgpu::BindGroupLayoutEntry {
                        binding: 0,
                        visibility: wgpu::ShaderStages::FRAGMENT,
                        ty: wgpu::BindingType::Texture {
                            multisampled: false,
                            view_dimension: wgpu::TextureViewDimension::D2,
                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 1,
                        visibility: wgpu::ShaderStages::FRAGMENT,
                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                        count: None,
                    },
                ],
                label: Some("texture_bind_group_layout"),
            });

        let render_pipeline_layout =
            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: Some("Render Pipeline Layout"),
                bind_group_layouts: &[Some(&texture_bind_group_layout), Some(&camera_layout)],
                immediate_size: 0,
            });

        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: descriptor.label,
            layout: Some(&render_pipeline_layout),

            vertex: wgpu::VertexState {
                module: &shader,
                entry_point: Some(descriptor.vertex_entry_point),
                buffers: &descriptor.vertex_layouts,
                compilation_options: wgpu::PipelineCompilationOptions::default(),
            },

            primitive: wgpu::PrimitiveState {
                topology: descriptor.topology,
                strip_index_format: None,
                front_face: descriptor.front_face,
                cull_mode: descriptor.cull_mode,
                polygon_mode: descriptor.polygon_mode,
                unclipped_depth: false,
                conservative: false,
            },

            depth_stencil: descriptor.depth_stencil,
            multisample: descriptor.multisample,

            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: Some(descriptor.fragment_entry_point),
                targets: &[Some(wgpu::ColorTargetState {
                    format: descriptor.format,
                    blend: descriptor.blend,
                    write_mask: descriptor.write_mask,
                })],
                compilation_options: wgpu::PipelineCompilationOptions::default(),
            }),

            multiview_mask: None,
            cache: None,
        });

        Ok(Self { render_pipeline })
    }

    /// Get inner pipeline value
    pub fn raw(&self) -> &wgpu::RenderPipeline {
        &self.render_pipeline
    }
}
