use crate::core::Rectangle;
use crate::graphics::Transformation;
use crate::layer;

use std::mem;
use wgpu::util::DeviceExt;

#[cfg(feature = "tracing")]
use tracing::info_span;

const INITIAL_INSTANCES: usize = 2_000;

#[derive(Debug)]
pub struct Pipeline {
    solid: solid::Pipeline,
    gradient: gradient::Pipeline,
    constant_layout: wgpu::BindGroupLayout,
    vertices: wgpu::Buffer,
    indices: wgpu::Buffer,
    layers: Vec<Layer>,
    prepare_layer: usize,
}

impl Pipeline {
    pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Pipeline {
        let constant_layout =
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                label: Some("iced_wgpu::quad uniforms layout"),
                entries: &[wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::VERTEX,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: wgpu::BufferSize::new(
                            mem::size_of::<Uniforms>() as wgpu::BufferAddress,
                        ),
                    },
                    count: None,
                }],
            });

        let vertices =
            device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some("iced_wgpu::quad vertex buffer"),
                contents: bytemuck::cast_slice(&VERTICES),
                usage: wgpu::BufferUsages::VERTEX,
            });

        let indices =
            device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some("iced_wgpu::quad index buffer"),
                contents: bytemuck::cast_slice(&INDICES),
                usage: wgpu::BufferUsages::INDEX,
            });

        Self {
            vertices,
            indices,
            solid: solid::Pipeline::new(device, format, &constant_layout),
            gradient: gradient::Pipeline::new(device, format, &constant_layout),
            layers: Vec::new(),
            prepare_layer: 0,
            constant_layout,
        }
    }

    pub fn prepare(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        instances: &layer::Quads,
        transformation: Transformation,
        scale: f32,
    ) {
        if self.layers.len() <= self.prepare_layer {
            self.layers.push(Layer::new(device, &self.constant_layout));
        }

        let layer = &mut self.layers[self.prepare_layer];
        layer.prepare(device, queue, instances, transformation, scale);

        self.prepare_layer += 1;
    }

    pub fn render<'a>(
        &'a self,
        layer: usize,
        bounds: Rectangle<u32>,
        render_pass: &mut wgpu::RenderPass<'a>,
    ) {
        if let Some(layer) = self.layers.get(layer) {
            render_pass.set_scissor_rect(
                bounds.x,
                bounds.y,
                bounds.width,
                bounds.height,
            );
            render_pass.set_index_buffer(
                self.indices.slice(..),
                wgpu::IndexFormat::Uint16,
            );
            render_pass.set_vertex_buffer(0, self.vertices.slice(..));

            if layer.solid.instance_count > 0 {
                render_pass.set_pipeline(&self.solid.pipeline);
                layer.solid.draw(&layer.constants, render_pass);
            }

            if layer.gradient.instance_count > 0 {
                render_pass.set_pipeline(&self.gradient.pipeline);
                layer.gradient.draw(&layer.constants, render_pass);
            }
        }
    }

    pub fn end_frame(&mut self) {
        self.prepare_layer = 0;
    }
}

#[derive(Debug)]
struct Layer {
    constants: wgpu::BindGroup,
    constants_buffer: wgpu::Buffer,
    solid: solid::Layer,
    gradient: gradient::Layer,
}

impl Layer {
    pub fn new(
        device: &wgpu::Device,
        constant_layout: &wgpu::BindGroupLayout,
    ) -> Self {
        let constants_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("iced_wgpu::quad uniforms buffer"),
            size: mem::size_of::<Uniforms>() as wgpu::BufferAddress,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        let constants = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("iced_wgpu::quad uniforms bind group"),
            layout: constant_layout,
            entries: &[wgpu::BindGroupEntry {
                binding: 0,
                resource: constants_buffer.as_entire_binding(),
            }],
        });

        Self {
            constants,
            constants_buffer,
            solid: solid::Layer::new(device),
            gradient: gradient::Layer::new(device),
        }
    }

    pub fn prepare(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        instances: &layer::Quads,
        transformation: Transformation,
        scale: f32,
    ) {
        #[cfg(feature = "tracing")]
        let _ = info_span!("Wgpu::Quad", "PREPARE").entered();

        let uniforms = Uniforms::new(transformation, scale);

        queue.write_buffer(
            &self.constants_buffer,
            0,
            bytemuck::bytes_of(&uniforms),
        );

        let _ = self.solid.instances.resize(device, instances.solids.len());
        let _ = self
            .gradient
            .instances
            .resize(device, instances.gradients.len());
        let _ =
            self.solid
                .instances
                .write(queue, 0, instances.solids.as_slice());
        self.solid.instance_count = instances.solids.len();
        let _ = self.gradient.instances.write(
            queue,
            0,
            instances.gradients.as_slice(),
        );
        self.gradient.instance_count = instances.gradients.len();
    }
}

mod solid {
    use crate::layer::quad;
    use crate::quad::{color_target_state, Vertex, INDICES, INITIAL_INSTANCES};
    use crate::Buffer;

    #[derive(Debug)]
    pub struct Pipeline {
        pub pipeline: wgpu::RenderPipeline,
    }

    #[derive(Debug)]
    pub struct Layer {
        pub instances: Buffer<quad::Solid>,
        pub instance_count: usize,
    }

    impl Layer {
        pub fn new(device: &wgpu::Device) -> Self {
            let instances = Buffer::new(
                device,
                "iced_wgpu.quad.solid.buffer",
                INITIAL_INSTANCES,
                wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
            );

            Self {
                instances,
                instance_count: 0,
            }
        }

        pub fn draw<'a>(
            &'a self,
            constants: &'a wgpu::BindGroup,
            render_pass: &mut wgpu::RenderPass<'a>,
        ) {
            #[cfg(feature = "tracing")]
            let _ = tracing::info_span!("Wgpu::Quad::Solid", "DRAW").entered();

            render_pass.set_bind_group(0, constants, &[]);
            render_pass.set_vertex_buffer(1, self.instances.slice(..));

            render_pass.draw_indexed(
                0..INDICES.len() as u32,
                0,
                0..self.instance_count as u32,
            );
        }
    }

    impl Pipeline {
        pub fn new(
            device: &wgpu::Device,
            format: wgpu::TextureFormat,
            constants_layout: &wgpu::BindGroupLayout,
        ) -> Self {
            let layout = device.create_pipeline_layout(
                &wgpu::PipelineLayoutDescriptor {
                    label: Some("iced_wgpu.quad.solid.pipeline"),
                    push_constant_ranges: &[],
                    bind_group_layouts: &[constants_layout],
                },
            );

            let shader =
                device.create_shader_module(wgpu::ShaderModuleDescriptor {
                    label: Some("iced_wgpu.quad.solid.shader"),
                    source: wgpu::ShaderSource::Wgsl(
                        std::borrow::Cow::Borrowed(include_str!(
                            "shader/quad.wgsl"
                        )),
                    ),
                });

            let pipeline = device.create_render_pipeline(
                &wgpu::RenderPipelineDescriptor {
                    label: Some("iced_wgpu.quad.solid.pipeline"),
                    layout: Some(&layout),
                    vertex: wgpu::VertexState {
                        module: &shader,
                        entry_point: "solid_vs_main",
                        buffers: &[
                            Vertex::buffer_layout(),
                            wgpu::VertexBufferLayout {
                                array_stride: std::mem::size_of::<quad::Solid>()
                                    as u64,
                                step_mode: wgpu::VertexStepMode::Instance,
                                attributes: &wgpu::vertex_attr_array!(
                                    // Color
                                    1 => Float32x4,
                                    // Position
                                    2 => Float32x2,
                                    // Size
                                    3 => Float32x2,
                                    // Border color
                                    4 => Float32x4,
                                    // Border radius
                                    5 => Float32x4,
                                    // Border width
                                    6 => Float32,
                                ),
                            },
                        ],
                    },
                    fragment: Some(wgpu::FragmentState {
                        module: &shader,
                        entry_point: "solid_fs_main",
                        targets: &color_target_state(format),
                    }),
                    primitive: wgpu::PrimitiveState {
                        topology: wgpu::PrimitiveTopology::TriangleList,
                        front_face: wgpu::FrontFace::Cw,
                        ..Default::default()
                    },
                    depth_stencil: None,
                    multisample: wgpu::MultisampleState {
                        count: 1,
                        mask: !0,
                        alpha_to_coverage_enabled: false,
                    },
                    multiview: None,
                },
            );

            Self { pipeline }
        }
    }
}

mod gradient {
    use crate::layer::quad;
    use crate::quad::{color_target_state, Vertex, INDICES, INITIAL_INSTANCES};
    use crate::Buffer;

    #[derive(Debug)]
    pub struct Pipeline {
        pub pipeline: wgpu::RenderPipeline,
    }

    #[derive(Debug)]
    pub struct Layer {
        pub instances: Buffer<quad::Gradient>,
        pub instance_count: usize,
    }

    impl Layer {
        pub fn new(device: &wgpu::Device) -> Self {
            let instances = Buffer::new(
                device,
                "iced_wgpu.quad.gradient.buffer",
                INITIAL_INSTANCES,
                wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
            );

            Self {
                instances,
                instance_count: 0,
            }
        }

        pub fn draw<'a>(
            &'a self,
            constants: &'a wgpu::BindGroup,
            render_pass: &mut wgpu::RenderPass<'a>,
        ) {
            #[cfg(feature = "tracing")]
            let _ =
                tracing::info_span!("Wgpu::Quad::Gradient", "DRAW").entered();

            render_pass.set_bind_group(0, constants, &[]);
            render_pass.set_vertex_buffer(1, self.instances.slice(..));

            render_pass.draw_indexed(
                0..INDICES.len() as u32,
                0,
                0..self.instance_count as u32,
            );
        }
    }

    impl Pipeline {
        pub fn new(
            device: &wgpu::Device,
            format: wgpu::TextureFormat,
            constants_layout: &wgpu::BindGroupLayout,
        ) -> Self {
            let layout = device.create_pipeline_layout(
                &wgpu::PipelineLayoutDescriptor {
                    label: Some("iced_wgpu.quad.gradient.pipeline"),
                    push_constant_ranges: &[],
                    bind_group_layouts: &[constants_layout],
                },
            );

            let shader =
                device.create_shader_module(wgpu::ShaderModuleDescriptor {
                    label: Some("iced_wgpu.quad.gradient.shader"),
                    source: wgpu::ShaderSource::Wgsl(
                        std::borrow::Cow::Borrowed(include_str!(
                            "shader/quad.wgsl"
                        )),
                    ),
                });

            let pipeline =
                device.create_render_pipeline(
                    &wgpu::RenderPipelineDescriptor {
                        label: Some("iced_wgpu.quad.gradient.pipeline"),
                        layout: Some(&layout),
                        vertex: wgpu::VertexState {
                            module: &shader,
                            entry_point: "gradient_vs_main",
                            buffers: &[
                                Vertex::buffer_layout(),
                                wgpu::VertexBufferLayout {
                                    array_stride: std::mem::size_of::<
                                        quad::Gradient,
                                    >(
                                    )
                                        as u64,
                                    step_mode: wgpu::VertexStepMode::Instance,
                                    attributes: &wgpu::vertex_attr_array!(
                                        // Color 1
                                        1 => Float32x4,
                                        // Color 2
                                        2 => Float32x4,
                                        // Color 3
                                        3 => Float32x4,
                                        // Color 4
                                        4 => Float32x4,
                                        // Color 5
                                        5 => Float32x4,
                                        // Color 6
                                        6 => Float32x4,
                                        // Color 7
                                        7 => Float32x4,
                                        // Color 8
                                        8 => Float32x4,
                                        // Offsets 1-4
                                        9 => Float32x4,
                                        // Offsets 5-8
                                        10 => Float32x4,
                                        // Direction
                                        11 => Float32x4,
                                        // Position & Scale
                                        12 => Float32x4,
                                        // Border color
                                        13 => Float32x4,
                                        // Border radius
                                        14 => Float32x4,
                                        // Border width
                                        15 => Float32
                                    ),
                                },
                            ],
                        },
                        fragment: Some(wgpu::FragmentState {
                            module: &shader,
                            entry_point: "gradient_fs_main",
                            targets: &color_target_state(format),
                        }),
                        primitive: wgpu::PrimitiveState {
                            topology: wgpu::PrimitiveTopology::TriangleList,
                            front_face: wgpu::FrontFace::Cw,
                            ..Default::default()
                        },
                        depth_stencil: None,
                        multisample: wgpu::MultisampleState {
                            count: 1,
                            mask: !0,
                            alpha_to_coverage_enabled: false,
                        },
                        multiview: None,
                    },
                );

            Self { pipeline }
        }
    }
}

fn color_target_state(
    format: wgpu::TextureFormat,
) -> [Option<wgpu::ColorTargetState>; 1] {
    [Some(wgpu::ColorTargetState {
        format,
        blend: 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,
            },
        }),
        write_mask: wgpu::ColorWrites::ALL,
    })]
}

#[repr(C)]
#[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
pub struct Vertex {
    _position: [f32; 2],
}

impl Vertex {
    fn buffer_layout<'a>() -> wgpu::VertexBufferLayout<'a> {
        wgpu::VertexBufferLayout {
            array_stride: mem::size_of::<Self>() as u64,
            step_mode: wgpu::VertexStepMode::Vertex,
            attributes: &[wgpu::VertexAttribute {
                shader_location: 0,
                format: wgpu::VertexFormat::Float32x2,
                offset: 0,
            }],
        }
    }
}

const INDICES: [u16; 6] = [0, 1, 2, 0, 2, 3];

const VERTICES: [Vertex; 4] = [
    Vertex {
        _position: [0.0, 0.0],
    },
    Vertex {
        _position: [1.0, 0.0],
    },
    Vertex {
        _position: [1.0, 1.0],
    },
    Vertex {
        _position: [0.0, 1.0],
    },
];

#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
struct Uniforms {
    transform: [f32; 16],
    scale: f32,
    // Uniforms must be aligned to their largest member,
    // this uses a mat4x4<f32> which aligns to 16, so align to that
    _padding: [f32; 3],
}

impl Uniforms {
    fn new(transformation: Transformation, scale: f32) -> Uniforms {
        Self {
            transform: *transformation.as_ref(),
            scale,
            _padding: [0.0; 3],
        }
    }
}

impl Default for Uniforms {
    fn default() -> Self {
        Self {
            transform: *Transformation::identity().as_ref(),
            scale: 1.0,
            _padding: [0.0; 3],
        }
    }
}
