// Render pipeline builder.
// Eliminates boilerplate for creating standard vertex/fragment pipelines.

use crate::formats::{DEPTH_FORMAT, TEXTURE_FORMAT};

/// Blend mode presets.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlendMode {
    /// Standard alpha blending (src_alpha, 1-src_alpha).
    Alpha,
    /// Additive blending (src_alpha, one).
    Additive,
    /// No blending.
    Opaque,
}

impl BlendMode {
    fn to_blend_state(self) -> Option<wgpu::BlendState> {
        match self {
            Self::Alpha => Some(wgpu::BlendState {
                color: wgpu::BlendComponent {
                    src_factor: wgpu::BlendFactor::SrcAlpha,
                    dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
                    operation: wgpu::BlendOperation::Add,
                },
                alpha: wgpu::BlendComponent {
                    src_factor: wgpu::BlendFactor::One,
                    dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
                    operation: wgpu::BlendOperation::Add,
                },
            }),
            Self::Additive => Some(wgpu::BlendState {
                color: wgpu::BlendComponent {
                    src_factor: wgpu::BlendFactor::SrcAlpha,
                    dst_factor: wgpu::BlendFactor::One,
                    operation: wgpu::BlendOperation::Add,
                },
                alpha: wgpu::BlendComponent {
                    src_factor: wgpu::BlendFactor::One,
                    dst_factor: wgpu::BlendFactor::One,
                    operation: wgpu::BlendOperation::Add,
                },
            }),
            Self::Opaque => None,
        }
    }
}

/// Topology preset matching common use cases.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Topology {
    Triangles,
    TriangleStrip,
    Lines,
    LineStrip,
    Points,
}

impl Topology {
    fn to_wgpu(self) -> wgpu::PrimitiveTopology {
        match self {
            Self::Triangles => wgpu::PrimitiveTopology::TriangleList,
            Self::TriangleStrip => wgpu::PrimitiveTopology::TriangleStrip,
            Self::Lines => wgpu::PrimitiveTopology::LineList,
            Self::LineStrip => wgpu::PrimitiveTopology::LineStrip,
            Self::Points => wgpu::PrimitiveTopology::PointList,
        }
    }
}

/// Builder for standard vertex/fragment render pipelines.
pub struct PipelineBuilder<'a> {
    label: &'a str,
    module: &'a wgpu::ShaderModule,
    vs_entry: &'a str,
    fs_entry: &'a str,
    layouts: &'a [Option<&'a wgpu::BindGroupLayout>],
    vertex_layouts: Vec<wgpu::VertexBufferLayout<'a>>,
    topology: Topology,
    blend: BlendMode,
    depth_write: bool,
    depth_test: bool,
    cull_mode: Option<wgpu::Face>,
    color_format: wgpu::TextureFormat,
    sample_count: u32,
}

impl<'a> PipelineBuilder<'a> {
    pub fn new(
        label: &'a str,
        module: &'a wgpu::ShaderModule,
        layouts: &'a [Option<&'a wgpu::BindGroupLayout>],
    ) -> Self {
        Self {
            label,
            module,
            vs_entry: "vs_main",
            fs_entry: "fs_main",
            layouts,
            vertex_layouts: Vec::new(),
            topology: Topology::Triangles,
            blend: BlendMode::Alpha,
            depth_write: true,
            depth_test: true,
            cull_mode: None,
            color_format: TEXTURE_FORMAT,
            sample_count: 1,
        }
    }

    pub fn vs_entry(mut self, entry: &'a str) -> Self {
        self.vs_entry = entry;
        self
    }

    pub fn fs_entry(mut self, entry: &'a str) -> Self {
        self.fs_entry = entry;
        self
    }

    pub fn topology(mut self, topo: Topology) -> Self {
        self.topology = topo;
        self
    }

    pub fn blend(mut self, mode: BlendMode) -> Self {
        self.blend = mode;
        self
    }

    pub fn depth_write(mut self, write: bool) -> Self {
        self.depth_write = write;
        self
    }

    pub fn depth_test(mut self, test: bool) -> Self {
        self.depth_test = test;
        self
    }

    pub fn cull_back(mut self) -> Self {
        self.cull_mode = Some(wgpu::Face::Back);
        self
    }

    pub fn color_format(mut self, format: wgpu::TextureFormat) -> Self {
        self.color_format = format;
        self
    }

    pub fn sample_count(mut self, count: u32) -> Self {
        self.sample_count = count;
        self
    }

    pub fn vertex_buffer(mut self, layout: wgpu::VertexBufferLayout<'a>) -> Self {
        self.vertex_layouts.push(layout);
        self
    }

    /// Build the wgpu RenderPipeline.
    pub fn build(self, device: &wgpu::Device) -> wgpu::RenderPipeline {
        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some(self.label),
            bind_group_layouts: self.layouts,
            immediate_size: 0,
        });

        let depth_stencil = if self.depth_test {
            Some(wgpu::DepthStencilState {
                format: DEPTH_FORMAT,
                depth_write_enabled: Some(self.depth_write),
                depth_compare: Some(wgpu::CompareFunction::Less),
                stencil: wgpu::StencilState::default(),
                bias: wgpu::DepthBiasState::default(),
            })
        } else {
            None
        };

        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some(self.label),
            layout: Some(&pipeline_layout),
            vertex: wgpu::VertexState {
                module: self.module,
                entry_point: Some(self.vs_entry),
                compilation_options: Default::default(),
                buffers: &self.vertex_layouts,
            },
            fragment: Some(wgpu::FragmentState {
                module: self.module,
                entry_point: Some(self.fs_entry),
                compilation_options: Default::default(),
                targets: &[Some(wgpu::ColorTargetState {
                    format: self.color_format,
                    blend: self.blend.to_blend_state(),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
            }),
            primitive: wgpu::PrimitiveState {
                topology: self.topology.to_wgpu(),
                front_face: wgpu::FrontFace::Ccw,
                cull_mode: self.cull_mode,
                ..Default::default()
            },
            depth_stencil,
            multisample: wgpu::MultisampleState {
                count: self.sample_count,
                ..Default::default()
            },
            multiview_mask: None,
            cache: None,
        })
    }
}

const FULLSCREEN_ATTRS: [wgpu::VertexAttribute; 1] = wgpu::vertex_attr_array![0 => Float32x2];
const POSITION3D_ATTRS: [wgpu::VertexAttribute; 1] = wgpu::vertex_attr_array![0 => Float32x3];
const POS_NORMAL_ATTRS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3];

/// Create a fullscreen quad vertex buffer layout (position: float32x2).
pub fn fullscreen_vertex_layout() -> wgpu::VertexBufferLayout<'static> {
    wgpu::VertexBufferLayout {
        array_stride: 2 * 4,
        step_mode: wgpu::VertexStepMode::Vertex,
        attributes: &FULLSCREEN_ATTRS,
    }
}

/// Create a 3D vertex buffer layout (position: float32x3).
pub fn position3d_vertex_layout() -> wgpu::VertexBufferLayout<'static> {
    wgpu::VertexBufferLayout {
        array_stride: 3 * 4,
        step_mode: wgpu::VertexStepMode::Vertex,
        attributes: &POSITION3D_ATTRS,
    }
}

/// Create a position+normal vertex buffer layout (float32x3 + float32x3).
pub fn position_normal_vertex_layout() -> wgpu::VertexBufferLayout<'static> {
    wgpu::VertexBufferLayout {
        array_stride: 6 * 4,
        step_mode: wgpu::VertexStepMode::Vertex,
        attributes: &POS_NORMAL_ATTRS,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn blend_mode_alpha() {
        assert!(BlendMode::Alpha.to_blend_state().is_some());
    }

    #[test]
    fn blend_mode_opaque() {
        assert!(BlendMode::Opaque.to_blend_state().is_none());
    }

    #[test]
    fn topology_mapping() {
        assert_eq!(Topology::Lines.to_wgpu(), wgpu::PrimitiveTopology::LineList);
    }
}
