use std::sync::Arc;

use rend3::{
    format_sso,
    resources::{CameraManager, MaterialManager, MeshBuffers, ObjectManager},
    ModeData,
};
use wgpu::{BindGroup, CommandEncoder, Device, RenderPass, RenderPipeline};
use wgpu_profiler::GpuProfiler;

use crate::{
    common::{interfaces::ShaderInterfaces, samplers::Samplers},
    culling::{
        cpu::{CpuCuller, CpuCullerCullArgs},
        gpu::{GpuCuller, GpuCullerCullArgs, PreCulledBuffer},
        CulledObjectSet, Sorting,
    },
    material::{PbrMaterial, TransparencyType},
};

use super::culling;

pub struct ForwardPassCullArgs<'a> {
    pub device: &'a Device,
    pub profiler: &'a mut GpuProfiler,
    pub encoder: &'a mut CommandEncoder,

    pub culler: ModeData<&'a CpuCuller, &'a GpuCuller>,

    pub interfaces: &'a ShaderInterfaces,

    pub camera: &'a CameraManager,
    pub objects: &'a mut ObjectManager,

    pub culling_input: ModeData<(), &'a mut PreCulledBuffer>,
}

pub struct ForwardPassPrepassArgs<'rpass, 'b> {
    pub device: &'b Device,
    pub profiler: &'b mut GpuProfiler,
    pub rpass: &'b mut RenderPass<'rpass>,

    pub materials: &'rpass MaterialManager,
    pub meshes: &'rpass MeshBuffers,

    pub samplers: &'rpass Samplers,
    pub texture_bg: ModeData<(), &'rpass BindGroup>,

    pub culled_objects: &'rpass CulledObjectSet,
}

pub struct ForwardPassDrawArgs<'rpass, 'b> {
    pub device: &'b Device,
    pub profiler: &'b mut GpuProfiler,
    pub rpass: &'b mut RenderPass<'rpass>,

    pub materials: &'rpass MaterialManager,
    pub meshes: &'rpass MeshBuffers,

    pub samplers: &'rpass Samplers,
    pub directional_light_bg: &'rpass BindGroup,
    pub texture_bg: ModeData<(), &'rpass BindGroup>,
    pub shader_uniform_bg: &'rpass BindGroup,

    pub culled_objects: &'rpass CulledObjectSet,
}

pub struct ForwardPass {
    depth_pipeline: Option<Arc<RenderPipeline>>,
    forward_pipeline: Arc<RenderPipeline>,
    transparency: TransparencyType,
}

impl ForwardPass {
    pub fn new(
        depth_pipeline: Option<Arc<RenderPipeline>>,
        forward_pipeline: Arc<RenderPipeline>,
        transparency: TransparencyType,
    ) -> Self {
        Self {
            depth_pipeline,
            forward_pipeline,
            transparency,
        }
    }

    pub fn cull(&self, args: ForwardPassCullArgs<'_>) -> CulledObjectSet {
        let label = format_sso!("forward cull {}", self.transparency.to_debug_str());
        profiling::scope!(&label);

        let sort = if self.transparency == TransparencyType::Blend {
            Some(Sorting::BackToFront)
        } else {
            None
        };

        match args.culler {
            ModeData::CPU(cpu_culler) => cpu_culler.cull(CpuCullerCullArgs {
                device: args.device,
                camera: args.camera,
                interfaces: args.interfaces,
                objects: args.objects,
                transparency: self.transparency,
                sort,
            }),
            ModeData::GPU(gpu_culler) => {
                args.profiler.begin_scope(&label, args.encoder, args.device);
                let culled = gpu_culler.cull(GpuCullerCullArgs {
                    device: args.device,
                    encoder: args.encoder,
                    interfaces: args.interfaces,
                    camera: args.camera,
                    input_buffer: args.culling_input.as_gpu(),
                    sort,
                });
                args.profiler.end_scope(args.encoder);
                culled
            }
        }
    }

    pub fn prepass<'rpass>(&'rpass self, args: ForwardPassPrepassArgs<'rpass, '_>) {
        let label = self.transparency.to_debug_str();
        profiling::scope!(label);
        args.profiler.begin_scope(label, args.rpass, args.device);

        args.meshes.bind(args.rpass);

        args.rpass.set_pipeline(
            self.depth_pipeline
                .as_ref()
                .expect("prepass called on a forward pass with no depth pipeline"),
        );
        args.rpass.set_bind_group(0, &args.samplers.linear_nearest_bg, &[]);
        args.rpass.set_bind_group(1, &args.culled_objects.output_bg, &[]);

        match args.culled_objects.calls {
            ModeData::CPU(ref draws) => culling::cpu::run(args.rpass, draws, args.samplers, 0, args.materials, 2),
            ModeData::GPU(ref data) => {
                args.rpass
                    .set_bind_group(2, args.materials.get_bind_group_gpu::<PbrMaterial>(), &[]);
                args.rpass.set_bind_group(3, args.texture_bg.as_gpu(), &[]);
                culling::gpu::run(args.rpass, data);
            }
        }
        args.profiler.end_scope(args.rpass);
    }

    pub fn draw<'rpass>(&'rpass self, args: ForwardPassDrawArgs<'rpass, '_>) {
        let label = self.transparency.to_debug_str();
        profiling::scope!(label);
        args.profiler.begin_scope(label, args.rpass, args.device);

        args.meshes.bind(args.rpass);

        args.rpass.set_pipeline(&self.forward_pipeline);
        args.rpass.set_bind_group(0, &args.samplers.linear_nearest_bg, &[]);
        args.rpass.set_bind_group(1, &args.culled_objects.output_bg, &[]);
        args.rpass.set_bind_group(2, args.directional_light_bg, &[]);
        args.rpass.set_bind_group(3, args.shader_uniform_bg, &[]);

        match args.culled_objects.calls {
            ModeData::CPU(ref draws) => culling::cpu::run(args.rpass, draws, args.samplers, 0, args.materials, 4),
            ModeData::GPU(ref data) => {
                args.rpass
                    .set_bind_group(4, args.materials.get_bind_group_gpu::<PbrMaterial>(), &[]);
                args.rpass.set_bind_group(5, args.texture_bg.as_gpu(), &[]);
                culling::gpu::run(args.rpass, data);
            }
        }

        args.profiler.end_scope(args.rpass);
    }
}
