#[path = "../framework.rs"]
mod framework;

use std::borrow::Cow;

const RENDER_TARGET_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;

struct Example {
    low_res_target: wgpu::TextureView,
    bind_group_upscale: wgpu::BindGroup,

    pipeline_triangle_conservative: wgpu::RenderPipeline,
    pipeline_triangle_regular: wgpu::RenderPipeline,
    pipeline_upscale: wgpu::RenderPipeline,
    pipeline_lines: Option<wgpu::RenderPipeline>,
    bind_group_layout_upscale: wgpu::BindGroupLayout,
}

impl Example {
    fn create_low_res_target(
        config: &wgpu::SurfaceConfiguration,
        device: &wgpu::Device,
        bind_group_layout_upscale: &wgpu::BindGroupLayout,
    ) -> (wgpu::TextureView, wgpu::BindGroup) {
        let texture_view = device
            .create_texture(&wgpu::TextureDescriptor {
                label: Some("Low Resolution Target"),
                size: wgpu::Extent3d {
                    width: config.width / 16,
                    height: config.width / 16,
                    depth_or_array_layers: 1,
                },
                mip_level_count: 1,
                sample_count: 1,
                dimension: wgpu::TextureDimension::D2,
                format: RENDER_TARGET_FORMAT,
                usage: wgpu::TextureUsages::TEXTURE_BINDING
                    | wgpu::TextureUsages::RENDER_ATTACHMENT,
            })
            .create_view(&Default::default());

        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("Nearest Neighbor Sampler"),
            mag_filter: wgpu::FilterMode::Nearest,
            min_filter: wgpu::FilterMode::Nearest,
            ..Default::default()
        });

        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("upscale bind group"),
            layout: bind_group_layout_upscale,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(&texture_view),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::Sampler(&sampler),
                },
            ],
        });

        (texture_view, bind_group)
    }
}

impl framework::Example for Example {
    fn required_features() -> wgpu::Features {
        wgpu::Features::CONSERVATIVE_RASTERIZATION
    }
    fn optional_features() -> wgpu::Features {
        wgpu::Features::NON_FILL_POLYGON_MODE
    }
    fn init(
        config: &wgpu::SurfaceConfiguration,
        _adapter: &wgpu::Adapter,
        device: &wgpu::Device,
        _queue: &wgpu::Queue,
    ) -> Self {
        let pipeline_layout_empty =
            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: None,
                bind_group_layouts: &[],
                push_constant_ranges: &[],
            });

        let shader_triangle_and_lines =
            device.create_shader_module(&wgpu::ShaderModuleDescriptor {
                label: None,
                source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
                    "triangle_and_lines.wgsl"
                ))),
            });

        let pipeline_triangle_conservative =
            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
                label: Some("Conservative Rasterization"),
                layout: Some(&pipeline_layout_empty),
                vertex: wgpu::VertexState {
                    module: &shader_triangle_and_lines,
                    entry_point: "vs_main",
                    buffers: &[],
                },
                fragment: Some(wgpu::FragmentState {
                    module: &shader_triangle_and_lines,
                    entry_point: "fs_main_red",
                    targets: &[RENDER_TARGET_FORMAT.into()],
                }),
                primitive: wgpu::PrimitiveState {
                    conservative: true,
                    ..Default::default()
                },
                depth_stencil: None,
                multisample: wgpu::MultisampleState::default(),
            });

        let pipeline_triangle_regular =
            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
                label: Some("Regular Rasterization"),
                layout: Some(&pipeline_layout_empty),
                vertex: wgpu::VertexState {
                    module: &shader_triangle_and_lines,
                    entry_point: "vs_main",
                    buffers: &[],
                },
                fragment: Some(wgpu::FragmentState {
                    module: &shader_triangle_and_lines,
                    entry_point: "fs_main_blue",
                    targets: &[RENDER_TARGET_FORMAT.into()],
                }),
                primitive: wgpu::PrimitiveState::default(),
                depth_stencil: None,
                multisample: wgpu::MultisampleState::default(),
            });

        let pipeline_lines = if device
            .features()
            .contains(wgpu::Features::NON_FILL_POLYGON_MODE)
        {
            Some(
                device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
                    label: Some("Lines"),
                    layout: Some(&pipeline_layout_empty),
                    vertex: wgpu::VertexState {
                        module: &shader_triangle_and_lines,
                        entry_point: "vs_main",
                        buffers: &[],
                    },
                    fragment: Some(wgpu::FragmentState {
                        module: &shader_triangle_and_lines,
                        entry_point: "fs_main_white",
                        targets: &[config.format.into()],
                    }),
                    primitive: wgpu::PrimitiveState {
                        polygon_mode: wgpu::PolygonMode::Line,
                        topology: wgpu::PrimitiveTopology::LineStrip,
                        ..Default::default()
                    },
                    depth_stencil: None,
                    multisample: wgpu::MultisampleState::default(),
                }),
            )
        } else {
            None
        };

        let (pipeline_upscale, bind_group_layout_upscale) = {
            let bind_group_layout =
                device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                    label: Some("upscale bindgroup"),
                    entries: &[
                        wgpu::BindGroupLayoutEntry {
                            binding: 0,
                            visibility: wgpu::ShaderStages::FRAGMENT,
                            ty: wgpu::BindingType::Texture {
                                sample_type: wgpu::TextureSampleType::Float { filterable: false },
                                view_dimension: wgpu::TextureViewDimension::D2,
                                multisampled: false,
                            },
                            count: None,
                        },
                        wgpu::BindGroupLayoutEntry {
                            binding: 1,
                            visibility: wgpu::ShaderStages::FRAGMENT,
                            ty: wgpu::BindingType::Sampler {
                                filtering: false,
                                comparison: false,
                            },
                            count: None,
                        },
                    ],
                });

            let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: None,
                bind_group_layouts: &[&bind_group_layout],
                push_constant_ranges: &[],
            });
            let shader = device.create_shader_module(&wgpu::ShaderModuleDescriptor {
                label: None,
                source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("upscale.wgsl"))),
            });
            (
                device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
                    label: Some("Upscale"),
                    layout: Some(&pipeline_layout),
                    vertex: wgpu::VertexState {
                        module: &shader,
                        entry_point: "vs_main",
                        buffers: &[],
                    },
                    fragment: Some(wgpu::FragmentState {
                        module: &shader,
                        entry_point: "fs_main",
                        targets: &[config.format.into()],
                    }),
                    primitive: wgpu::PrimitiveState::default(),
                    depth_stencil: None,
                    multisample: wgpu::MultisampleState::default(),
                }),
                bind_group_layout,
            )
        };

        let (low_res_target, bind_group_upscale) =
            Self::create_low_res_target(config, device, &bind_group_layout_upscale);

        Self {
            low_res_target,
            bind_group_upscale,

            pipeline_triangle_conservative,
            pipeline_triangle_regular,
            pipeline_upscale,
            pipeline_lines,
            bind_group_layout_upscale,
        }
    }

    fn resize(
        &mut self,
        config: &wgpu::SurfaceConfiguration,
        device: &wgpu::Device,
        _queue: &wgpu::Queue,
    ) {
        let (low_res_target, bind_group_upscale) =
            Self::create_low_res_target(config, device, &self.bind_group_layout_upscale);
        self.low_res_target = low_res_target;
        self.bind_group_upscale = bind_group_upscale;
    }

    fn update(&mut self, _event: winit::event::WindowEvent) {}

    fn render(
        &mut self,
        view: &wgpu::TextureView,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        _spawner: &framework::Spawner,
    ) {
        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
            label: Some("primary"),
        });

        {
            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("low resolution"),
                color_attachments: &[wgpu::RenderPassColorAttachment {
                    view: &self.low_res_target,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
                        store: true,
                    },
                }],
                depth_stencil_attachment: None,
            });

            rpass.set_pipeline(&self.pipeline_triangle_conservative);
            rpass.draw(0..3, 0..1);
            rpass.set_pipeline(&self.pipeline_triangle_regular);
            rpass.draw(0..3, 0..1);
        }
        {
            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("full resolution"),
                color_attachments: &[wgpu::RenderPassColorAttachment {
                    view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
                        store: true,
                    },
                }],
                depth_stencil_attachment: None,
            });

            rpass.set_pipeline(&self.pipeline_upscale);
            rpass.set_bind_group(0, &self.bind_group_upscale, &[]);
            rpass.draw(0..3, 0..1);

            if let Some(pipeline_lines) = &self.pipeline_lines {
                rpass.set_pipeline(pipeline_lines);
                rpass.draw(0..4, 0..1);
            }
        }

        queue.submit(Some(encoder.finish()));
    }
}

fn main() {
    framework::run::<Example>("conservative-raster");
}

#[test]
fn conservative_raster() {
    framework::test::<Example>(framework::FrameworkRefTest {
        image_path: "/examples/conservative-raster/screenshot.png",
        width: 1024,
        height: 768,
        optional_features: wgpu::Features::default(),
        base_test_parameters: framework::test_common::TestParameters::default(),
        tolerance: 0,
        max_outliers: 0,
    });
}
