use super::grass::{GpuGrassType, MAX_TYPES, gpu_types};
use super::projection::calculate_cascade_shadows;
use crate::ecs::terrain::{
    TERRAIN_CACHE_SIZE, TERRAIN_COLLIDER_LAYER, TERRAIN_GRASS_LEVEL, TerrainShare, TerrainSnapshot,
    TerrainSnapshotState,
};
use crate::ecs::world::World;
use crate::render::wgpu::rendergraph::{PassExecutionContext, PassNode};
use nalgebra_glm::{Mat4, Vec3};
use wgpu::util::DeviceExt;

const MAX_LEVELS: usize = 12;
const TILE_TEXELS: i32 = 64;
const BLOCK_CELLS: u32 = 32;
const BLOCK_INDEX_COUNT: u32 = BLOCK_CELLS * BLOCK_CELLS * 6;
const MAX_JOBS: usize = 1024;
const MAX_INSTANCES: usize = 512;
const SNAPSHOT_BYTES: u64 = (TERRAIN_CACHE_SIZE as u64) * (TERRAIN_CACHE_SIZE as u64) * 4;

#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct TerrainProduceUniforms {
    continental_spline: [[f32; 4]; 4],
    erosion_spline: [[f32; 4]; 4],
    frequencies: [f32; 4],
    warp_detail: [f32; 4],
    erosion_params: [f32; 4],
    seeds: [u32; 4],
}

#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct TerrainRenderUniforms {
    view_projection: [[f32; 4]; 4],
    camera_position: [f32; 4],
    sun_direction: [f32; 4],
    sun_color: [f32; 4],
    ambient: [f32; 4],
    params: [f32; 4],
    splat: [f32; 4],
    rock_color: [f32; 4],
    snow_color: [f32; 4],
    wind_time: [f32; 4],
    frustum_planes: [[f32; 4]; 6],
    level_regions: [[f32; 4]; 12],
    cascade_view_projections: [[[f32; 4]; 4]; 4],
    cascade_splits: [f32; 4],
    cascade_atlas_offsets: [[f32; 4]; 4],
    cascade_atlas_scale: [f32; 4],
}

pub struct TerrainPass {
    produce_uniforms_buffer: wgpu::Buffer,
    render_uniforms_buffer: wgpu::Buffer,
    types_buffer: wgpu::Buffer,
    jobs_buffer: wgpu::Buffer,
    instances_buffer: wgpu::Buffer,
    index_buffer: wgpu::Buffer,
    cache_view: wgpu::TextureView,
    cache_texture: wgpu::Texture,
    ambient_view: wgpu::TextureView,
    shadow_sampler: wgpu::Sampler,
    draw_indirect_buffer: wgpu::Buffer,
    produce_pipeline: wgpu::ComputePipeline,
    ambient_pipeline: wgpu::ComputePipeline,
    cull_pipeline: wgpu::ComputePipeline,
    render_pipeline: wgpu::RenderPipeline,
    produce_bind_group: wgpu::BindGroup,
    ambient_bind_group: wgpu::BindGroup,
    cull_bind_group: wgpu::BindGroup,
    render_bind_group_layout: wgpu::BindGroupLayout,
    render_bind_group: Option<wgpu::BindGroup>,
    share: Option<TerrainShare>,
    level_windows: Vec<Option<[i32; 2]>>,
    collider_window: Option<[i32; 2]>,
    pending_jobs: Vec<[i32; 4]>,
    job_count: u32,
    settings_revision: Option<u64>,
    grass_types_revision: Option<u64>,
    encode_snapshot: Option<([i32; 2], u64)>,
    time: f32,
    active: bool,
}

impl TerrainPass {
    pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
        let produce_uniforms_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("Terrain Produce Uniforms"),
            size: std::mem::size_of::<TerrainProduceUniforms>() as u64,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        let render_uniforms_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("Terrain Render Uniforms"),
            size: std::mem::size_of::<TerrainRenderUniforms>() as u64,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        let types_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("Terrain Grass Types Buffer"),
            size: (std::mem::size_of::<GpuGrassType>() * MAX_TYPES) as u64,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        let jobs_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("Terrain Jobs Buffer"),
            size: (MAX_JOBS * 16) as u64,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        let instances_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("Terrain Instances Buffer"),
            size: (MAX_INSTANCES * 16) as u64,
            usage: wgpu::BufferUsages::STORAGE,
            mapped_at_creation: false,
        });

        let draw_indirect_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("Terrain Draw Indirect Buffer"),
            size: 20,
            usage: wgpu::BufferUsages::STORAGE
                | wgpu::BufferUsages::INDIRECT
                | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        let indices = build_block_indices();
        let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("Terrain Index Buffer"),
            contents: bytemuck::cast_slice(&indices),
            usage: wgpu::BufferUsages::INDEX,
        });

        let cache_texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("Terrain Height Cache"),
            size: wgpu::Extent3d {
                width: TERRAIN_CACHE_SIZE,
                height: TERRAIN_CACHE_SIZE,
                depth_or_array_layers: MAX_LEVELS as u32,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::R32Float,
            usage: wgpu::TextureUsages::STORAGE_BINDING
                | wgpu::TextureUsages::TEXTURE_BINDING
                | wgpu::TextureUsages::COPY_SRC,
            view_formats: &[],
        });
        let cache_view = cache_texture.create_view(&wgpu::TextureViewDescriptor {
            dimension: Some(wgpu::TextureViewDimension::D2Array),
            ..Default::default()
        });

        let ambient_texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("Terrain Ambient Cache"),
            size: wgpu::Extent3d {
                width: TERRAIN_CACHE_SIZE,
                height: TERRAIN_CACHE_SIZE,
                depth_or_array_layers: MAX_LEVELS as u32,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::R32Float,
            usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        let ambient_view = ambient_texture.create_view(&wgpu::TextureViewDescriptor {
            dimension: Some(wgpu::TextureViewDimension::D2Array),
            ..Default::default()
        });

        let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("Terrain Shadow Sampler"),
            mag_filter: wgpu::FilterMode::Nearest,
            min_filter: wgpu::FilterMode::Nearest,
            address_mode_u: wgpu::AddressMode::ClampToEdge,
            address_mode_v: wgpu::AddressMode::ClampToEdge,
            ..Default::default()
        });

        let produce_bind_group_layout =
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                label: Some("Terrain Produce Bind Group Layout"),
                entries: &[
                    wgpu::BindGroupLayoutEntry {
                        binding: 0,
                        visibility: wgpu::ShaderStages::COMPUTE,
                        ty: wgpu::BindingType::Buffer {
                            ty: wgpu::BufferBindingType::Uniform,
                            has_dynamic_offset: false,
                            min_binding_size: None,
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 1,
                        visibility: wgpu::ShaderStages::COMPUTE,
                        ty: wgpu::BindingType::Buffer {
                            ty: wgpu::BufferBindingType::Storage { read_only: true },
                            has_dynamic_offset: false,
                            min_binding_size: None,
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 2,
                        visibility: wgpu::ShaderStages::COMPUTE,
                        ty: wgpu::BindingType::StorageTexture {
                            access: wgpu::StorageTextureAccess::WriteOnly,
                            format: wgpu::TextureFormat::R32Float,
                            view_dimension: wgpu::TextureViewDimension::D2Array,
                        },
                        count: None,
                    },
                ],
            });

        let produce_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("Terrain Produce Bind Group"),
            layout: &produce_bind_group_layout,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: produce_uniforms_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: jobs_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: wgpu::BindingResource::TextureView(&cache_view),
                },
            ],
        });

        let ambient_bind_group_layout =
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                label: Some("Terrain Ambient Bind Group Layout"),
                entries: &[
                    wgpu::BindGroupLayoutEntry {
                        binding: 0,
                        visibility: wgpu::ShaderStages::COMPUTE,
                        ty: wgpu::BindingType::Buffer {
                            ty: wgpu::BufferBindingType::Uniform,
                            has_dynamic_offset: false,
                            min_binding_size: None,
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 1,
                        visibility: wgpu::ShaderStages::COMPUTE,
                        ty: wgpu::BindingType::Buffer {
                            ty: wgpu::BufferBindingType::Storage { read_only: true },
                            has_dynamic_offset: false,
                            min_binding_size: None,
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 2,
                        visibility: wgpu::ShaderStages::COMPUTE,
                        ty: wgpu::BindingType::Texture {
                            multisampled: false,
                            view_dimension: wgpu::TextureViewDimension::D2Array,
                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 3,
                        visibility: wgpu::ShaderStages::COMPUTE,
                        ty: wgpu::BindingType::StorageTexture {
                            access: wgpu::StorageTextureAccess::WriteOnly,
                            format: wgpu::TextureFormat::R32Float,
                            view_dimension: wgpu::TextureViewDimension::D2Array,
                        },
                        count: None,
                    },
                ],
            });

        let ambient_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("Terrain Ambient Bind Group"),
            layout: &ambient_bind_group_layout,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: produce_uniforms_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: jobs_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: wgpu::BindingResource::TextureView(&cache_view),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: wgpu::BindingResource::TextureView(&ambient_view),
                },
            ],
        });

        let render_stage = wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT;
        let render_bind_group_layout =
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                label: Some("Terrain Render Bind Group Layout"),
                entries: &[
                    wgpu::BindGroupLayoutEntry {
                        binding: 0,
                        visibility: render_stage,
                        ty: wgpu::BindingType::Buffer {
                            ty: wgpu::BufferBindingType::Uniform,
                            has_dynamic_offset: false,
                            min_binding_size: None,
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 1,
                        visibility: render_stage,
                        ty: wgpu::BindingType::Buffer {
                            ty: wgpu::BufferBindingType::Storage { read_only: true },
                            has_dynamic_offset: false,
                            min_binding_size: None,
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 2,
                        visibility: render_stage,
                        ty: wgpu::BindingType::Texture {
                            multisampled: false,
                            view_dimension: wgpu::TextureViewDimension::D2Array,
                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 3,
                        visibility: wgpu::ShaderStages::FRAGMENT,
                        ty: wgpu::BindingType::Buffer {
                            ty: wgpu::BufferBindingType::Storage { read_only: true },
                            has_dynamic_offset: false,
                            min_binding_size: None,
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 4,
                        visibility: wgpu::ShaderStages::FRAGMENT,
                        ty: wgpu::BindingType::Texture {
                            multisampled: false,
                            view_dimension: wgpu::TextureViewDimension::D2Array,
                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 5,
                        visibility: wgpu::ShaderStages::FRAGMENT,
                        ty: wgpu::BindingType::Texture {
                            multisampled: false,
                            view_dimension: wgpu::TextureViewDimension::D2,
                            sample_type: wgpu::TextureSampleType::Depth,
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 6,
                        visibility: wgpu::ShaderStages::FRAGMENT,
                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
                        count: None,
                    },
                ],
            });

        let produce_shader = crate::render::wgpu::shader_compose::compile_wgsl(
            device,
            "Terrain Produce Shader",
            concat!(
                include_str!("../../shaders/terrain_common.wgsl"),
                include_str!("../../shaders/terrain_produce.wgsl"),
            ),
        );

        let render_shader = crate::render::wgpu::shader_compose::compile_wgsl(
            device,
            "Terrain Render Shader",
            concat!(
                include_str!("../../shaders/terrain_common.wgsl"),
                include_str!("../../shaders/grass_common.wgsl"),
                include_str!("../../shaders/terrain_render.wgsl"),
            ),
        );

        let produce_pipeline_layout =
            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: Some("Terrain Produce Pipeline Layout"),
                bind_group_layouts: &[Some(&produce_bind_group_layout)],
                immediate_size: 0,
            });

        let produce_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("Terrain Produce Pipeline"),
            layout: Some(&produce_pipeline_layout),
            module: &produce_shader,
            entry_point: Some("produce_tiles"),
            compilation_options: Default::default(),
            cache: None,
        });

        let ambient_shader = crate::render::wgpu::shader_compose::compile_wgsl(
            device,
            "Terrain Ambient Shader",
            concat!(
                include_str!("../../shaders/terrain_common.wgsl"),
                include_str!("../../shaders/terrain_ao.wgsl"),
            ),
        );

        let ambient_pipeline_layout =
            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: Some("Terrain Ambient Pipeline Layout"),
                bind_group_layouts: &[Some(&ambient_bind_group_layout)],
                immediate_size: 0,
            });

        let ambient_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("Terrain Ambient Pipeline"),
            layout: Some(&ambient_pipeline_layout),
            module: &ambient_shader,
            entry_point: Some("produce_ambient"),
            compilation_options: Default::default(),
            cache: None,
        });

        let cull_shader = crate::render::wgpu::shader_compose::compile_wgsl(
            device,
            "Terrain Cull Shader",
            include_str!("../../shaders/terrain_cull.wgsl"),
        );

        let cull_bind_group_layout =
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                label: Some("Terrain Cull Bind Group Layout"),
                entries: &[
                    wgpu::BindGroupLayoutEntry {
                        binding: 0,
                        visibility: wgpu::ShaderStages::COMPUTE,
                        ty: wgpu::BindingType::Buffer {
                            ty: wgpu::BufferBindingType::Uniform,
                            has_dynamic_offset: false,
                            min_binding_size: None,
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 1,
                        visibility: wgpu::ShaderStages::COMPUTE,
                        ty: wgpu::BindingType::Buffer {
                            ty: wgpu::BufferBindingType::Storage { read_only: false },
                            has_dynamic_offset: false,
                            min_binding_size: None,
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 2,
                        visibility: wgpu::ShaderStages::COMPUTE,
                        ty: wgpu::BindingType::Buffer {
                            ty: wgpu::BufferBindingType::Storage { read_only: false },
                            has_dynamic_offset: false,
                            min_binding_size: None,
                        },
                        count: None,
                    },
                ],
            });

        let cull_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("Terrain Cull Bind Group"),
            layout: &cull_bind_group_layout,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: render_uniforms_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: instances_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: draw_indirect_buffer.as_entire_binding(),
                },
            ],
        });

        let cull_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("Terrain Cull Pipeline Layout"),
            bind_group_layouts: &[Some(&cull_bind_group_layout)],
            immediate_size: 0,
        });

        let cull_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("Terrain Cull Pipeline"),
            layout: Some(&cull_pipeline_layout),
            module: &cull_shader,
            entry_point: Some("cull_blocks"),
            compilation_options: Default::default(),
            cache: None,
        });

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

        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("Terrain Render Pipeline"),
            layout: Some(&render_pipeline_layout),
            vertex: wgpu::VertexState {
                module: &render_shader,
                entry_point: Some("vs_terrain"),
                buffers: &[],
                compilation_options: Default::default(),
            },
            fragment: Some(wgpu::FragmentState {
                module: &render_shader,
                entry_point: Some("fs_terrain"),
                targets: &[
                    Some(wgpu::ColorTargetState {
                        format: color_format,
                        blend: None,
                        write_mask: wgpu::ColorWrites::ALL,
                    }),
                    Some(wgpu::ColorTargetState {
                        format: wgpu::TextureFormat::R32Float,
                        blend: None,
                        write_mask: wgpu::ColorWrites::ALL,
                    }),
                ],
                compilation_options: Default::default(),
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                strip_index_format: None,
                front_face: wgpu::FrontFace::Ccw,
                cull_mode: None,
                polygon_mode: wgpu::PolygonMode::Fill,
                unclipped_depth: false,
                conservative: false,
            },
            depth_stencil: Some(wgpu::DepthStencilState {
                format: wgpu::TextureFormat::Depth32Float,
                depth_write_enabled: Some(true),
                depth_compare: Some(wgpu::CompareFunction::GreaterEqual),
                stencil: wgpu::StencilState::default(),
                bias: wgpu::DepthBiasState::default(),
            }),
            multisample: wgpu::MultisampleState::default(),
            multiview_mask: None,
            cache: None,
        });

        Self {
            produce_uniforms_buffer,
            render_uniforms_buffer,
            types_buffer,
            jobs_buffer,
            instances_buffer,
            index_buffer,
            cache_view,
            cache_texture,
            ambient_view,
            shadow_sampler,
            draw_indirect_buffer,
            produce_pipeline,
            ambient_pipeline,
            cull_pipeline,
            render_pipeline,
            produce_bind_group,
            ambient_bind_group,
            cull_bind_group,
            render_bind_group_layout,
            render_bind_group: None,
            share: None,
            level_windows: vec![None; MAX_LEVELS],
            collider_window: None,
            pending_jobs: Vec::new(),
            job_count: 0,
            settings_revision: None,
            grass_types_revision: None,
            encode_snapshot: None,
            time: 0.0,
            active: false,
        }
    }
}

fn build_block_indices() -> Vec<u32> {
    let mut indices = Vec::with_capacity(BLOCK_INDEX_COUNT as usize);
    for cell_z in 0..BLOCK_CELLS {
        for cell_x in 0..BLOCK_CELLS {
            let top_left = cell_z * (BLOCK_CELLS + 1) + cell_x;
            let top_right = top_left + 1;
            let bottom_left = top_left + BLOCK_CELLS + 1;
            let bottom_right = bottom_left + 1;
            indices.extend_from_slice(&[
                top_left,
                bottom_left,
                top_right,
                top_right,
                bottom_left,
                bottom_right,
            ]);
        }
    }
    indices
}

fn pack_spline(points: &[[f32; 2]; 8]) -> [[f32; 4]; 4] {
    let mut output = [[0.0f32; 4]; 4];
    for (index, point) in points.iter().enumerate() {
        let slot = &mut output[index / 2];
        let lane = (index % 2) * 2;
        slot[lane] = point[0];
        slot[lane + 1] = point[1];
    }
    output
}

type DirectionalLight = (
    crate::ecs::light::components::Light,
    crate::ecs::transform::components::GlobalTransform,
);

fn query_sun(world: &World) -> (Vec3, Vec3, Option<DirectionalLight>) {
    for entity in world
        .core
        .query_entities(crate::ecs::world::LIGHT | crate::ecs::world::GLOBAL_TRANSFORM)
    {
        if let (Some(light), Some(transform)) = (
            world.core.get_light(entity),
            world.core.get_global_transform(entity),
        ) && light.light_type == crate::ecs::light::components::LightType::Directional
        {
            let toward_light = -transform.forward_vector();
            let color = light.color * light.intensity;
            let directional = if light.cast_shadows {
                Some((light.clone(), *transform))
            } else {
                None
            };
            return (toward_light.normalize(), color, directional);
        }
    }
    (
        Vec3::new(0.35, 0.8, 0.35).normalize(),
        Vec3::new(4.0, 3.8, 3.2),
        None,
    )
}

impl PassNode<World> for TerrainPass {
    fn name(&self) -> &str {
        "terrain_pass"
    }

    fn reads(&self) -> Vec<&str> {
        vec!["shadow_depth"]
    }

    fn writes(&self) -> Vec<&str> {
        vec![]
    }

    fn reads_writes(&self) -> Vec<&str> {
        vec!["color", "depth", "entity_id"]
    }

    fn invalidate_bind_groups(&mut self) {
        self.render_bind_group = None;
    }

    fn prepare(&mut self, _device: &wgpu::Device, queue: &wgpu::Queue, world: &World) {
        if self.share.is_none() {
            self.share = Some(world.resources.terrain_render.share.clone());
        }
        let share = self.share.as_ref().expect("terrain share").clone();

        share.with(|shared| {
            if let Some(snapshot) = shared.snapshot.as_mut()
                && matches!(snapshot.state, TerrainSnapshotState::Encoded)
            {
                let map_done = snapshot.map_done.clone();
                snapshot
                    .buffer
                    .slice(..)
                    .map_async(wgpu::MapMode::Read, move |_| {
                        map_done.store(true, std::sync::atomic::Ordering::Relaxed);
                    });
                snapshot.state = TerrainSnapshotState::Mapping;
            }
        });

        let settings = &world.resources.terrain;
        self.active = settings.enabled;
        if !self.active {
            share.with(|shared| {
                shared.cache_ready = false;
            });
            self.encode_snapshot = None;
            return;
        }
        let Some(camera_matrices) =
            crate::ecs::camera::queries::query_active_camera_matrices(world)
        else {
            self.active = false;
            return;
        };

        if self.settings_revision != Some(settings.revision) {
            self.level_windows = vec![None; MAX_LEVELS];
            self.collider_window = None;
            self.pending_jobs.clear();
            self.settings_revision = Some(settings.revision);
            let produce_uniforms = TerrainProduceUniforms {
                continental_spline: pack_spline(&settings.continental_spline),
                erosion_spline: pack_spline(&settings.erosion_spline),
                frequencies: [
                    settings.continental_frequency,
                    settings.erosion_frequency,
                    settings.ridge_frequency,
                    settings.warp_frequency,
                ],
                warp_detail: [
                    settings.warp_strength,
                    settings.detail_amplitude,
                    settings.texel_size,
                    settings.origin_flatten_radius,
                ],
                erosion_params: [settings.erosion_sharpness, 0.0, 0.0, 0.0],
                seeds: [settings.seed, 0, 0, 0],
            };
            queue.write_buffer(
                &self.produce_uniforms_buffer,
                0,
                bytemuck::bytes_of(&produce_uniforms),
            );
        }

        let view: Mat4 = camera_matrices.view;
        let projection: Mat4 = camera_matrices.projection;
        let view_projection = projection * view;
        let view_inverse = nalgebra_glm::inverse(&view);
        let camera_position = Vec3::new(
            view_inverse[(0, 3)],
            view_inverse[(1, 3)],
            view_inverse[(2, 3)],
        );

        let level_count = (settings.level_count as usize).clamp(2, TERRAIN_COLLIDER_LAYER as usize);
        let cache_size = TERRAIN_CACHE_SIZE as i32;
        let half_cache = cache_size / 2;

        for level in 0..level_count {
            let texel = settings.texel_size * (1u32 << level) as f32;
            let tile_world = texel * TILE_TEXELS as f32;
            let window_origin = [
                (camera_position.x / tile_world).floor() as i32 * TILE_TEXELS - half_cache,
                (camera_position.z / tile_world).floor() as i32 * TILE_TEXELS - half_cache,
            ];
            let previous = self.level_windows[level];
            if previous == Some(window_origin) {
                continue;
            }
            for tile_z in 0..(cache_size / TILE_TEXELS) {
                for tile_x in 0..(cache_size / TILE_TEXELS) {
                    let origin = [
                        window_origin[0] + tile_x * TILE_TEXELS,
                        window_origin[1] + tile_z * TILE_TEXELS,
                    ];
                    let covered = previous.is_some_and(|old| {
                        origin[0] >= old[0]
                            && origin[1] >= old[1]
                            && origin[0] + TILE_TEXELS <= old[0] + cache_size
                            && origin[1] + TILE_TEXELS <= old[1] + cache_size
                    });
                    if !covered {
                        self.pending_jobs
                            .push([origin[0], origin[1], level as i32, level as i32]);
                    }
                }
            }
            self.level_windows[level] = Some(window_origin);
        }

        let collider_request = share.with(|shared| {
            if shared.snapshot.is_none() {
                shared.snapshot_request.take()
            } else {
                None
            }
        });
        self.encode_snapshot = None;
        if let Some(request_center) = collider_request {
            let grass_texel = settings.texel_size * (1u32 << TERRAIN_GRASS_LEVEL) as f32;
            let tile_world = grass_texel * TILE_TEXELS as f32;
            let window_origin = [
                (request_center[0] / tile_world).floor() as i32 * TILE_TEXELS - half_cache,
                (request_center[1] / tile_world).floor() as i32 * TILE_TEXELS - half_cache,
            ];
            if self.collider_window != Some(window_origin) {
                let previous = self.collider_window;
                for tile_z in 0..(cache_size / TILE_TEXELS) {
                    for tile_x in 0..(cache_size / TILE_TEXELS) {
                        let origin = [
                            window_origin[0] + tile_x * TILE_TEXELS,
                            window_origin[1] + tile_z * TILE_TEXELS,
                        ];
                        let covered = previous.is_some_and(|old| {
                            origin[0] >= old[0]
                                && origin[1] >= old[1]
                                && origin[0] + TILE_TEXELS <= old[0] + cache_size
                                && origin[1] + TILE_TEXELS <= old[1] + cache_size
                        });
                        if !covered {
                            self.pending_jobs.push([
                                origin[0],
                                origin[1],
                                TERRAIN_GRASS_LEVEL as i32,
                                TERRAIN_COLLIDER_LAYER as i32,
                            ]);
                        }
                    }
                }
                self.collider_window = Some(window_origin);
            }
            self.encode_snapshot = Some((window_origin, settings.revision));
        }

        let job_count = self.pending_jobs.len().min(MAX_JOBS);
        if job_count > 0 {
            let jobs: Vec<[i32; 4]> = self.pending_jobs.drain(..job_count).collect();
            queue.write_buffer(&self.jobs_buffer, 0, bytemuck::cast_slice(&jobs));
        }
        self.job_count = job_count as u32;

        let mut level_regions = [[0.0f32; 4]; 12];
        for (level, region) in level_regions.iter_mut().enumerate().take(level_count) {
            let block_world =
                (settings.texel_size * (1u32 << level) as f32 * BLOCK_CELLS as f32) as f64;
            let snap = block_world * 2.0;
            let origin = [
                (camera_position.x as f64 / snap).floor() * snap - snap,
                (camera_position.z as f64 / snap).floor() * snap - snap,
            ];
            let half_extent = snap * 1.5;
            *region = [
                (origin[0] + half_extent) as f32,
                (origin[1] + half_extent) as f32,
                half_extent as f32,
                0.0,
            ];
        }
        let frustum_planes = super::extract_frustum_planes(&view_projection);

        queue.write_buffer(
            &self.draw_indirect_buffer,
            0,
            bytemuck::cast_slice(&[BLOCK_INDEX_COUNT, 0, 0, 0, 0]),
        );

        self.time += world.resources.window.timing.delta_time;
        let wind = &world.resources.wind;
        let wind_planar = nalgebra_glm::Vec2::new(wind.direction.x, wind.direction.z);
        let wind_direction = if wind_planar.norm() > f32::EPSILON {
            wind_planar.normalize()
        } else {
            nalgebra_glm::Vec2::new(1.0, 0.0)
        };
        let (sun_direction, sun_color, directional_light) = query_sun(world);
        let ambient = world.resources.render_settings.ambient_light;
        let shadows_wanted = world
            .resources
            .renderer_state
            .active_view
            .shadow_depth_enabled
            && directional_light.is_some();
        let mut cascade_view_projections = [[[0.0f32; 4]; 4]; 4];
        let mut cascade_splits = [0.0f32; 4];
        let mut cascade_atlas_offsets = [[0.0f32; 4]; 4];
        let mut shadows_enabled = 0.0f32;
        let mut shadow_bias = 0.0f32;
        if shadows_wanted {
            let cascade_result = calculate_cascade_shadows(world, directional_light.as_ref());
            cascade_view_projections = cascade_result.cascade_view_projections;
            cascade_splits = cascade_result.cascade_split_distances;
            shadow_bias = cascade_result.shadow_bias;
            shadows_enabled = cascade_result.shadows_enabled;
            let slot_resolution = crate::render::wgpu::passes::CASCADE_SLOT_RESOLUTION;
            let slots = [[0.0, 0.0], [0.5, 0.0], [0.0, 0.5], [0.5, 0.5]];
            for cascade_index in 0..4 {
                cascade_atlas_offsets[cascade_index] = [
                    slots[cascade_index][0],
                    slots[cascade_index][1],
                    cascade_result.cascade_diameters[cascade_index] / slot_resolution,
                    0.0,
                ];
            }
        }
        let render_uniforms = TerrainRenderUniforms {
            view_projection: view_projection.into(),
            camera_position: [camera_position.x, camera_position.y, camera_position.z, 0.0],
            sun_direction: [sun_direction.x, sun_direction.y, sun_direction.z, 0.0],
            sun_color: [sun_color.x, sun_color.y, sun_color.z, 0.0],
            ambient: [
                ambient[0] * ambient[3],
                ambient[1] * ambient[3],
                ambient[2] * ambient[3],
                0.0,
            ],
            params: [
                settings.texel_size,
                level_count as f32,
                settings.height_min,
                settings.height_max,
            ],
            splat: [
                settings.rock_slope,
                settings.snow_height,
                world.resources.grass.types.len().min(MAX_TYPES) as f32,
                world.resources.grass.type_noise_scale.max(1.0),
            ],
            rock_color: [
                settings.rock_color[0],
                settings.rock_color[1],
                settings.rock_color[2],
                world.resources.grass.clump_scale.max(0.2),
            ],
            snow_color: settings.snow_color,
            wind_time: [
                wind_direction.x,
                wind_direction.y,
                wind.strength * 0.12,
                self.time,
            ],
            frustum_planes: frustum_planes.map(|plane| [plane.x, plane.y, plane.z, plane.w]),
            level_regions,
            cascade_view_projections,
            cascade_splits,
            cascade_atlas_offsets,
            cascade_atlas_scale: [0.5, 0.5, shadows_enabled, shadow_bias],
        };
        queue.write_buffer(
            &self.render_uniforms_buffer,
            0,
            bytemuck::bytes_of(&render_uniforms),
        );

        if self.grass_types_revision != Some(world.resources.grass.types_revision) {
            let types = gpu_types(&world.resources.grass);
            queue.write_buffer(&self.types_buffer, 0, bytemuck::cast_slice(&types));
            self.grass_types_revision = Some(world.resources.grass.types_revision);
        }

        let grass_level = TERRAIN_GRASS_LEVEL as usize;
        let grass_origin = self.level_windows[grass_level].unwrap_or([0, 0]);
        let grass_texel_size = settings.texel_size * (1u32 << TERRAIN_GRASS_LEVEL) as f32;
        let cache_view = self.cache_view.clone();
        share.with(|shared| {
            if shared.cache_view.is_none() {
                shared.cache_view = Some(cache_view);
            }
            shared.cache_ready = true;
            shared.grass_origin_texels = grass_origin;
            shared.grass_texel_size = grass_texel_size;
        });
    }

    fn execute<'r, 'e>(
        &mut self,
        context: PassExecutionContext<'r, 'e, World>,
    ) -> crate::render::wgpu::rendergraph::Result<
        Vec<crate::render::wgpu::rendergraph::SubGraphRunCommand<'r>>,
    > {
        if !context.is_pass_enabled() || !self.active {
            return Ok(context.into_sub_graph_commands());
        }

        if self.job_count > 0 {
            let mut compute_pass =
                context
                    .encoder
                    .begin_compute_pass(&wgpu::ComputePassDescriptor {
                        label: Some("Terrain Produce Pass"),
                        timestamp_writes: None,
                    });
            compute_pass.set_pipeline(&self.produce_pipeline);
            compute_pass.set_bind_group(0, &self.produce_bind_group, &[]);
            compute_pass.dispatch_workgroups(8, 8, self.job_count);
            compute_pass.set_pipeline(&self.ambient_pipeline);
            compute_pass.set_bind_group(0, &self.ambient_bind_group, &[]);
            compute_pass.dispatch_workgroups(8, 8, self.job_count);
        }

        {
            let mut compute_pass =
                context
                    .encoder
                    .begin_compute_pass(&wgpu::ComputePassDescriptor {
                        label: Some("Terrain Cull Pass"),
                        timestamp_writes: None,
                    });
            compute_pass.set_pipeline(&self.cull_pipeline);
            compute_pass.set_bind_group(0, &self.cull_bind_group, &[]);
            compute_pass.dispatch_workgroups((12 * 36u32).div_ceil(64), 1, 1);
        }

        let (color_view, color_load, color_store) = context.get_color_attachment("color")?;
        let (depth_view, depth_load, depth_store) = context.get_depth_attachment("depth")?;
        let (entity_view, entity_load, entity_store) = context.get_color_attachment("entity_id")?;

        if self.render_bind_group.is_none() {
            let shadow_view = context.get_texture_view("shadow_depth")?;
            self.render_bind_group = Some(context.device.create_bind_group(
                &wgpu::BindGroupDescriptor {
                    label: Some("Terrain Render Bind Group"),
                    layout: &self.render_bind_group_layout,
                    entries: &[
                        wgpu::BindGroupEntry {
                            binding: 0,
                            resource: self.render_uniforms_buffer.as_entire_binding(),
                        },
                        wgpu::BindGroupEntry {
                            binding: 1,
                            resource: self.instances_buffer.as_entire_binding(),
                        },
                        wgpu::BindGroupEntry {
                            binding: 2,
                            resource: wgpu::BindingResource::TextureView(&self.cache_view),
                        },
                        wgpu::BindGroupEntry {
                            binding: 3,
                            resource: self.types_buffer.as_entire_binding(),
                        },
                        wgpu::BindGroupEntry {
                            binding: 4,
                            resource: wgpu::BindingResource::TextureView(&self.ambient_view),
                        },
                        wgpu::BindGroupEntry {
                            binding: 5,
                            resource: wgpu::BindingResource::TextureView(shadow_view),
                        },
                        wgpu::BindGroupEntry {
                            binding: 6,
                            resource: wgpu::BindingResource::Sampler(&self.shadow_sampler),
                        },
                    ],
                },
            ));
        }

        {
            let mut render_pass = context
                .encoder
                .begin_render_pass(&wgpu::RenderPassDescriptor {
                    label: Some("Terrain Render Pass"),
                    color_attachments: &[
                        Some(wgpu::RenderPassColorAttachment {
                            view: color_view,
                            resolve_target: None,
                            ops: wgpu::Operations {
                                load: color_load,
                                store: color_store,
                            },
                            depth_slice: None,
                        }),
                        Some(wgpu::RenderPassColorAttachment {
                            view: entity_view,
                            resolve_target: None,
                            ops: wgpu::Operations {
                                load: entity_load,
                                store: entity_store,
                            },
                            depth_slice: None,
                        }),
                    ],
                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
                        view: depth_view,
                        depth_ops: Some(wgpu::Operations {
                            load: depth_load,
                            store: depth_store,
                        }),
                        stencil_ops: None,
                    }),
                    timestamp_writes: None,
                    occlusion_query_set: None,
                    multiview_mask: None,
                });
            render_pass.set_pipeline(&self.render_pipeline);
            render_pass.set_bind_group(0, self.render_bind_group.as_ref(), &[]);
            render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
            render_pass.draw_indexed_indirect(&self.draw_indirect_buffer, 0);
        }

        if let Some((origin, revision)) = self.encode_snapshot.take()
            && let Some(share) = self.share.as_ref()
        {
            let buffer = context.device.create_buffer(&wgpu::BufferDescriptor {
                label: Some("Terrain Snapshot Buffer"),
                size: SNAPSHOT_BYTES,
                usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
                mapped_at_creation: false,
            });
            context.encoder.copy_texture_to_buffer(
                wgpu::TexelCopyTextureInfo {
                    texture: &self.cache_texture,
                    mip_level: 0,
                    origin: wgpu::Origin3d {
                        x: 0,
                        y: 0,
                        z: TERRAIN_COLLIDER_LAYER,
                    },
                    aspect: wgpu::TextureAspect::All,
                },
                wgpu::TexelCopyBufferInfo {
                    buffer: &buffer,
                    layout: wgpu::TexelCopyBufferLayout {
                        offset: 0,
                        bytes_per_row: Some(TERRAIN_CACHE_SIZE * 4),
                        rows_per_image: Some(TERRAIN_CACHE_SIZE),
                    },
                },
                wgpu::Extent3d {
                    width: TERRAIN_CACHE_SIZE,
                    height: TERRAIN_CACHE_SIZE,
                    depth_or_array_layers: 1,
                },
            );
            share.with(|shared| {
                shared.snapshot = Some(TerrainSnapshot {
                    buffer,
                    map_done: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
                    origin_texels: origin,
                    texel_size: shared.grass_texel_size,
                    revision,
                    state: TerrainSnapshotState::Encoded,
                });
            });
        }

        Ok(context.into_sub_graph_commands())
    }
}
