// SDF — Signed Distance Field tracing (software RT fallback).
//
// Provides SDF generation (Jump Flooding Algorithm) and sphere marching
// through a global distance field. Used when hardware ray tracing
// (EXPERIMENTAL_RAY_QUERY) is unavailable.
//
// Pipeline:
//   1. SDF Generate — Jump Flooding voxelization of mesh geometry into
//      per-mesh 3D SDF textures (R16Float, mesh_sdf_resolution^3 per mesh).
//   2. Global Distance Field merge — composites per-mesh SDFs into a
//      world-space volume (R16Float, global_resolution).
//   3. SDF Trace — sphere marching fullscreen compute through the GDF,
//      writing hit/miss + radiance for GI fallback.
//
// VRAM budget: ~70 MB at default config (128x128x64 GDF + 1920x1080 trace).

use crate::camera::Camera;

/// SDF trace uniforms (matches sdf_trace.wgsl SdfTraceUniforms).
///
/// 160 bytes: mat4 (64) + vec3+u32 (16) + vec2u+f32+f32 (16) = 96...
/// Actually: [f32;16]=64 + [f32;3]=12 + u32=4 + [u32;2]=8 + f32=4 + f32=4 = 96.
/// Pad to 160 for alignment headroom.
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct SdfTraceUniforms {
    /// Inverse view-projection matrix for ray reconstruction.
    pub view_proj_inv: [f32; 16],
    /// Camera world-space position.
    pub camera_position: [f32; 3],
    /// Maximum sphere march steps per ray.
    pub max_steps: u32,
    /// Screen dimensions in pixels.
    pub screen_size: [u32; 2],
    /// Maximum trace distance in world units.
    pub max_trace_distance: f32,
    /// Distance threshold to consider a hit.
    pub hit_threshold: f32,
    /// GDF volume world-space origin (lower corner).
    pub gdf_origin: [f32; 3],
    /// GDF volume world-space extent.
    pub gdf_extent: f32,
    /// GDF resolution for UVW computation.
    pub gdf_resolution: [u32; 3],
    /// Padding to 160 bytes.
    pub _pad: u32,
}

/// SDF generation uniforms (matches sdf_generate.wgsl SdfGenParams).
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct SdfGenParams {
    /// Volume resolution (x, y, z).
    pub resolution: [u32; 3],
    /// JFA step size (halves each pass).
    pub step_size: u32,
    /// Pass index: 0 = init seed, 1+ = JFA iterations.
    pub pass_index: u32,
    /// Padding.
    pub _pad: [u32; 3],
}

/// Configuration for SDF generation and tracing.
#[derive(Debug, Clone)]
pub struct SdfConfig {
    /// Per-mesh SDF volume resolution (cubic). Default: 64.
    pub mesh_sdf_resolution: u32,
    /// Global distance field resolution [x, y, z]. Default: [128, 128, 64].
    pub global_resolution: [u32; 3],
    /// Maximum meshes tracked in the SDF atlas. Default: 256.
    pub max_meshes: u32,
    /// Maximum trace distance in world units. Default: 500.0.
    pub max_trace_distance: f32,
    /// Maximum sphere march steps. Default: 128.
    pub max_steps: u32,
    /// Hit threshold distance. Default: 0.01.
    pub hit_threshold: f32,
    /// World-space extent of the GDF volume. Default: 256.0.
    pub gdf_extent: f32,
}

impl Default for SdfConfig {
    fn default() -> Self {
        Self {
            mesh_sdf_resolution: 64,
            global_resolution: [128, 128, 64],
            max_meshes: 256,
            max_trace_distance: 500.0,
            max_steps: 128,
            hit_threshold: 0.01,
            gdf_extent: 256.0,
        }
    }
}

/// SDF pass — software RT fallback via jump flooding + sphere marching.
///
/// Two compute passes per frame:
/// 1. **Generate** — JFA compute for mesh voxelization (run on mesh load, not per-frame).
/// 2. **Trace** — fullscreen sphere march through the GDF for GI/shadow fallback.
pub struct SdfPass {
    pub enabled: bool,
    // Global distance field (R16Float 3D volume)
    gdf_texture: Option<wgpu::Texture>,
    gdf_view: Option<wgpu::TextureView>,
    // Trace output (Rgba16Float 2D — hit radiance + distance)
    trace_output: Option<wgpu::Texture>,
    trace_output_view: Option<wgpu::TextureView>,
    // Uniform buffers
    trace_uniform_buffer: Option<wgpu::Buffer>,
    gen_uniform_buffer: Option<wgpu::Buffer>,
    // Pipelines
    generate_pipeline: Option<wgpu::ComputePipeline>,
    trace_pipeline: Option<wgpu::ComputePipeline>,
    // Bind group layouts (cached)
    generate_bgl: Option<wgpu::BindGroupLayout>,
    trace_bgl: Option<wgpu::BindGroupLayout>,
    // Sampler for GDF trilinear filtering
    sampler: Option<wgpu::Sampler>,
    // State
    config: SdfConfig,
    width: u32,
    height: u32,
    initialized: bool,
}

impl Default for SdfPass {
    fn default() -> Self {
        Self {
            enabled: false, // disabled by default — opt-in fallback
            gdf_texture: None,
            gdf_view: None,
            trace_output: None,
            trace_output_view: None,
            trace_uniform_buffer: None,
            gen_uniform_buffer: None,
            generate_pipeline: None,
            trace_pipeline: None,
            generate_bgl: None,
            trace_bgl: None,
            sampler: None,
            config: SdfConfig::default(),
            width: 0,
            height: 0,
            initialized: false,
        }
    }
}

impl SdfPass {
    /// Create an SDF pass with the given configuration.
    pub fn new(config: SdfConfig) -> Self {
        Self {
            enabled: true,
            config,
            ..Default::default()
        }
    }

    /// Initialize GPU resources: GDF volume, trace output, uniform buffers.
    /// Does not create pipelines — call `create_pipelines` separately.
    pub fn init(&mut self, device: &wgpu::Device, width: u32, height: u32) {
        if self.initialized && self.width == width && self.height == height {
            return;
        }

        let gdf_res = self.config.global_resolution;

        // Global distance field: R16Float 3D volume
        let gdf_tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("sdf_gdf"),
            size: wgpu::Extent3d {
                width: gdf_res[0],
                height: gdf_res[1],
                depth_or_array_layers: gdf_res[2],
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D3,
            format: wgpu::TextureFormat::R16Float,
            usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        self.gdf_view = Some(gdf_tex.create_view(&wgpu::TextureViewDescriptor::default()));
        self.gdf_texture = Some(gdf_tex);

        // Trace output: Rgba16Float 2D (hit radiance RGB + distance A)
        let trace_tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("sdf_trace_output"),
            size: wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba16Float,
            usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        self.trace_output_view = Some(trace_tex.create_view(&wgpu::TextureViewDescriptor::default()));
        self.trace_output = Some(trace_tex);

        // Trace uniform buffer
        if self.trace_uniform_buffer.is_none() {
            self.trace_uniform_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
                label: Some("sdf_trace_uniforms"),
                size: std::mem::size_of::<SdfTraceUniforms>() as u64,
                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
                mapped_at_creation: false,
            }));
        }

        // Generate uniform buffer
        if self.gen_uniform_buffer.is_none() {
            self.gen_uniform_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
                label: Some("sdf_gen_uniforms"),
                size: std::mem::size_of::<SdfGenParams>() as u64,
                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
                mapped_at_creation: false,
            }));
        }

        // GDF sampler (trilinear filtering for smooth distance field reads)
        if self.sampler.is_none() {
            self.sampler = Some(device.create_sampler(&wgpu::SamplerDescriptor {
                label: Some("sdf_sampler"),
                mag_filter: wgpu::FilterMode::Linear,
                min_filter: wgpu::FilterMode::Linear,
                ..Default::default()
            }));
        }

        self.width = width;
        self.height = height;
        self.initialized = true;

        log::info!(
            "SDF initialized (GDF {}x{}x{}, trace output {}x{})",
            gdf_res[0],
            gdf_res[1],
            gdf_res[2],
            width,
            height
        );
    }

    /// Create compute pipelines for SDF generation and tracing.
    pub fn create_pipelines(&mut self, device: &wgpu::Device) {
        // ── Generate pipeline (JFA) ──────────────────────────────────────

        let gen_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("sdf_gen_bgl"),
            entries: &[
                // binding 0: SdfGenParams uniform
                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,
                },
                // binding 1: sdf_read (storage read)
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::StorageTexture {
                        access: wgpu::StorageTextureAccess::ReadOnly,
                        format: wgpu::TextureFormat::R16Float,
                        view_dimension: wgpu::TextureViewDimension::D3,
                    },
                    count: None,
                },
                // binding 2: sdf_write (storage write)
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::StorageTexture {
                        access: wgpu::StorageTextureAccess::WriteOnly,
                        format: wgpu::TextureFormat::R16Float,
                        view_dimension: wgpu::TextureViewDimension::D3,
                    },
                    count: None,
                },
            ],
        });

        let gen_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("sdf_generate.wgsl"),
            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/sdf_generate.wgsl").into()),
        });

        let gen_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("sdf_gen_layout"),
            bind_group_layouts: &[Some(&gen_bgl)],
            immediate_size: 0,
        });

        self.generate_pipeline = Some(device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("sdf_generate"),
            layout: Some(&gen_layout),
            module: &gen_shader,
            entry_point: Some("sdf_generate"),
            compilation_options: Default::default(),
            cache: None,
        }));

        self.generate_bgl = Some(gen_bgl);

        // ── Trace pipeline (sphere marching) ─────────────────────────────

        let trace_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("sdf_trace_bgl"),
            entries: &[
                // binding 0: SdfTraceUniforms
                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,
                },
                // binding 1: GDF volume (texture_3d, sampled)
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        view_dimension: wgpu::TextureViewDimension::D3,
                        multisampled: false,
                    },
                    count: None,
                },
                // binding 2: GDF sampler
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                    count: None,
                },
                // binding 3: depth texture
                wgpu::BindGroupLayoutEntry {
                    binding: 3,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Depth,
                        view_dimension: wgpu::TextureViewDimension::D2,
                        multisampled: false,
                    },
                    count: None,
                },
                // binding 4: trace output (storage write)
                wgpu::BindGroupLayoutEntry {
                    binding: 4,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::StorageTexture {
                        access: wgpu::StorageTextureAccess::WriteOnly,
                        format: wgpu::TextureFormat::Rgba16Float,
                        view_dimension: wgpu::TextureViewDimension::D2,
                    },
                    count: None,
                },
            ],
        });

        let trace_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("sdf_trace.wgsl"),
            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/sdf_trace.wgsl").into()),
        });

        let trace_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("sdf_trace_layout"),
            bind_group_layouts: &[Some(&trace_bgl)],
            immediate_size: 0,
        });

        self.trace_pipeline = Some(device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("sdf_trace"),
            layout: Some(&trace_layout),
            module: &trace_shader,
            entry_point: Some("sdf_trace"),
            compilation_options: Default::default(),
            cache: None,
        }));

        self.trace_bgl = Some(trace_bgl);

        log::info!("SDF pipelines created (generate JFA + trace sphere march)");
    }

    /// Dispatch the JFA generation pass for the global distance field.
    ///
    /// Runs `ceil(log2(max_dim))` JFA passes to propagate distance values.
    /// Called on mesh load, not every frame.
    pub fn generate(&self, _device: &wgpu::Device, queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder) {
        if !self.enabled || !self.initialized {
            return;
        }
        let Some(ref pipeline) = self.generate_pipeline else {
            return;
        };
        let Some(ref bgl) = self.generate_bgl else { return };
        let Some(ref gen_buf) = self.gen_uniform_buffer else {
            return;
        };
        let Some(ref gdf_view) = self.gdf_view else { return };

        let res = self.config.global_resolution;
        let max_dim = res[0].max(res[1]).max(res[2]);
        let num_passes = (max_dim as f32).log2().ceil() as u32;

        // JFA: step sizes halve from max_dim/2 down to 1
        for pass_idx in 0..num_passes {
            let step_size = (max_dim / 2).max(1) >> pass_idx;

            let params = SdfGenParams {
                resolution: res,
                step_size,
                pass_index: pass_idx,
                _pad: [0; 3],
            };
            queue.write_buffer(gen_buf, 0, bytemuck::bytes_of(&params));

            // For JFA we read and write the same volume (ping-pong would be
            // ideal, but for simplicity we use read-after-write barriers
            // between passes via separate compute pass scopes).
            let bg = _device.create_bind_group(&wgpu::BindGroupDescriptor {
                label: Some("sdf_gen_bg"),
                layout: bgl,
                entries: &[
                    wgpu::BindGroupEntry {
                        binding: 0,
                        resource: gen_buf.as_entire_binding(),
                    },
                    wgpu::BindGroupEntry {
                        binding: 1,
                        resource: wgpu::BindingResource::TextureView(gdf_view),
                    },
                    wgpu::BindGroupEntry {
                        binding: 2,
                        resource: wgpu::BindingResource::TextureView(gdf_view),
                    },
                ],
            });

            {
                let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
                    label: Some("sdf_generate"),
                    timestamp_writes: None,
                });
                pass.set_pipeline(pipeline);
                pass.set_bind_group(0, &bg, &[]);
                pass.dispatch_workgroups(res[0].div_ceil(4), res[1].div_ceil(4), res[2].div_ceil(4));
            }
        }
    }

    /// Dispatch fullscreen sphere march through the GDF.
    /// Writes hit radiance + distance to trace output texture.
    pub fn trace(
        &self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        encoder: &mut wgpu::CommandEncoder,
        depth_view: &wgpu::TextureView,
        camera: &Camera,
    ) {
        if !self.enabled || !self.initialized {
            return;
        }
        let Some(ref pipeline) = self.trace_pipeline else {
            return;
        };
        let Some(ref bgl) = self.trace_bgl else { return };
        let Some(ref trace_buf) = self.trace_uniform_buffer else {
            return;
        };
        let Some(ref gdf_view) = self.gdf_view else { return };
        let Some(ref trace_view) = self.trace_output_view else {
            return;
        };
        let Some(ref sampler) = self.sampler else { return };

        let view_proj = camera.projection_matrix * camera.view_matrix;
        let view_proj_inv = view_proj.inverse();

        // GDF origin: center on camera XZ, extend half the volume
        let half_ext = self.config.gdf_extent * 0.5;
        let gdf_origin = [
            camera.position.x - half_ext,
            -half_ext * 0.5, // slight downward bias
            camera.position.z - half_ext,
        ];

        let uniforms = SdfTraceUniforms {
            view_proj_inv: view_proj_inv.to_cols_array(),
            camera_position: camera.position.to_array(),
            max_steps: self.config.max_steps,
            screen_size: [self.width, self.height],
            max_trace_distance: self.config.max_trace_distance,
            hit_threshold: self.config.hit_threshold,
            gdf_origin,
            gdf_extent: self.config.gdf_extent,
            gdf_resolution: self.config.global_resolution,
            _pad: 0,
        };
        queue.write_buffer(trace_buf, 0, bytemuck::bytes_of(&uniforms));

        let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("sdf_trace_bg"),
            layout: bgl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: trace_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::TextureView(gdf_view),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: wgpu::BindingResource::Sampler(sampler),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: wgpu::BindingResource::TextureView(depth_view),
                },
                wgpu::BindGroupEntry {
                    binding: 4,
                    resource: wgpu::BindingResource::TextureView(trace_view),
                },
            ],
        });

        {
            let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
                label: Some("sdf_trace"),
                timestamp_writes: None,
            });
            pass.set_pipeline(pipeline);
            pass.set_bind_group(0, &bg, &[]);
            pass.dispatch_workgroups(self.width.div_ceil(8), self.height.div_ceil(8), 1);
        }
    }

    /// Get the trace output texture view for external binding (GI fallback).
    pub fn trace_output_view(&self) -> Option<&wgpu::TextureView> {
        self.trace_output_view.as_ref()
    }

    /// Estimate total VRAM usage in bytes for the given config and screen resolution.
    pub fn vram_estimate(width: u32, height: u32, config: &SdfConfig) -> u64 {
        let gdf_res = config.global_resolution;

        // GDF volume: R16Float = 2 bytes per voxel
        let gdf_bytes = gdf_res[0] as u64 * gdf_res[1] as u64 * gdf_res[2] as u64 * 2;

        // Trace output: Rgba16Float = 8 bytes per pixel
        let trace_bytes = width as u64 * height as u64 * 8;

        // Uniform buffers
        let uniform_bytes = std::mem::size_of::<SdfTraceUniforms>() as u64 + std::mem::size_of::<SdfGenParams>() as u64;

        gdf_bytes + trace_bytes + uniform_bytes
    }

    /// Resize screen-dependent resources. GDF volume is resolution-independent.
    pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
        if self.width == width && self.height == height {
            return;
        }

        // Destroy old trace output
        if let Some(t) = self.trace_output.take() {
            t.destroy();
        }
        self.trace_output_view = None;

        // Recreate trace output at new resolution
        let trace_tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("sdf_trace_output"),
            size: wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba16Float,
            usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        self.trace_output_view = Some(trace_tex.create_view(&wgpu::TextureViewDescriptor::default()));
        self.trace_output = Some(trace_tex);

        self.width = width;
        self.height = height;

        log::info!("SDF trace resized ({}x{})", width, height);
    }

    /// Drop all GPU resources.
    pub fn destroy(&mut self) {
        if let Some(t) = self.gdf_texture.take() {
            t.destroy();
        }
        if let Some(t) = self.trace_output.take() {
            t.destroy();
        }
        if let Some(b) = self.trace_uniform_buffer.take() {
            b.destroy();
        }
        if let Some(b) = self.gen_uniform_buffer.take() {
            b.destroy();
        }
        self.gdf_view = None;
        self.trace_output_view = None;
        self.initialized = false;
    }
}

// ── Analytical SDF primitives ────────────────────────────────────────────
//
// CPU-side distance functions for primitive shapes. Used for:
// - Procedural voxelization (upload to GDF without mesh geometry)
// - Editor gizmo distance queries
// - Physics broadphase distance checks

/// Signed distance to a sphere.
pub fn sdf_sphere(p: [f32; 3], center: [f32; 3], radius: f32) -> f32 {
    let dx = p[0] - center[0];
    let dy = p[1] - center[1];
    let dz = p[2] - center[2];
    (dx * dx + dy * dy + dz * dz).sqrt() - radius
}

/// Signed distance to an axis-aligned box.
pub fn sdf_box(p: [f32; 3], center: [f32; 3], half_extents: [f32; 3]) -> f32 {
    let dx = (p[0] - center[0]).abs() - half_extents[0];
    let dy = (p[1] - center[1]).abs() - half_extents[1];
    let dz = (p[2] - center[2]).abs() - half_extents[2];

    let outside = (dx.max(0.0) * dx.max(0.0) + dy.max(0.0) * dy.max(0.0) + dz.max(0.0) * dz.max(0.0)).sqrt();
    let inside = dx.max(dy).max(dz).min(0.0);

    outside + inside
}

/// Signed distance to an infinite cylinder along the Y axis.
pub fn sdf_cylinder(p: [f32; 3], center: [f32; 3], radius: f32, half_height: f32) -> f32 {
    let dx = p[0] - center[0];
    let dz = p[2] - center[2];
    let dy = (p[1] - center[1]).abs() - half_height;

    let d_radial = (dx * dx + dz * dz).sqrt() - radius;
    let outside_r = d_radial.max(0.0);
    let outside_y = dy.max(0.0);
    let outside = (outside_r * outside_r + outside_y * outside_y).sqrt();
    let inside = d_radial.max(dy).min(0.0);

    outside + inside
}

/// Signed distance to a capsule (line segment with radius).
pub fn sdf_capsule(p: [f32; 3], a: [f32; 3], b: [f32; 3], radius: f32) -> f32 {
    let pa = [p[0] - a[0], p[1] - a[1], p[2] - a[2]];
    let ba = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];

    let ba_dot = ba[0] * ba[0] + ba[1] * ba[1] + ba[2] * ba[2];
    if ba_dot < f32::EPSILON {
        return sdf_sphere(p, a, radius);
    }

    let h = ((pa[0] * ba[0] + pa[1] * ba[1] + pa[2] * ba[2]) / ba_dot).clamp(0.0, 1.0);
    let dx = pa[0] - ba[0] * h;
    let dy = pa[1] - ba[1] * h;
    let dz = pa[2] - ba[2] * h;

    (dx * dx + dy * dy + dz * dz).sqrt() - radius
}

/// Signed distance to a torus (ring) in the XZ plane centered at `center`.
pub fn sdf_torus(p: [f32; 3], center: [f32; 3], major_radius: f32, minor_radius: f32) -> f32 {
    let dx = p[0] - center[0];
    let dy = p[1] - center[1];
    let dz = p[2] - center[2];

    let q_xz = (dx * dx + dz * dz).sqrt() - major_radius;
    (q_xz * q_xz + dy * dy).sqrt() - minor_radius
}

/// Signed distance to a cone with apex at `apex`, axis along -Y, half-angle `angle_rad`.
pub fn sdf_cone(p: [f32; 3], apex: [f32; 3], angle_rad: f32, height: f32) -> f32 {
    let dx = p[0] - apex[0];
    let dy = p[1] - apex[1];
    let dz = p[2] - apex[2];

    let q = (dx * dx + dz * dz).sqrt();
    let sin_a = angle_rad.sin();
    let cos_a = angle_rad.cos();

    // Project onto cone surface
    let d = q * cos_a + dy * sin_a;
    let d_side = q * sin_a - dy * cos_a;
    let d_bottom = dy + height;

    if d_side <= 0.0 && d_bottom >= 0.0 {
        // Inside or on the cone surface
        d.max(-d_bottom)
    } else {
        // Outside — approximate with distance to nearest feature
        let side_dist = d.max(0.0);
        let bottom_dist = (-d_bottom).max(0.0);
        (side_dist * side_dist + bottom_dist * bottom_dist).sqrt()
    }
}

/// Signed distance to a pyramid (4-sided, axis along Y, base centered at `center`).
pub fn sdf_pyramid(p: [f32; 3], center: [f32; 3], base_half: f32, height: f32) -> f32 {
    let dx = (p[0] - center[0]).abs();
    let dy = p[1] - center[1];
    let dz = (p[2] - center[2]).abs();

    // Reduce to 2D by symmetry (use max of dx, dz for the base distance)
    let q = dx.max(dz);
    let slope = height / base_half;
    let d_plane = q - base_half + dy * (base_half / height);
    let d_base = -dy;
    let d_cap = dy - height;

    if d_base <= 0.0 && d_cap <= 0.0 && d_plane <= 0.0 {
        d_plane.max(d_base).max(d_cap)
    } else {
        let _ = slope; // used in derivation
        let outside_base = d_base.max(0.0);
        let outside_cap = d_cap.max(0.0);
        let outside_plane = d_plane.max(0.0);
        (outside_base * outside_base + outside_cap * outside_cap + outside_plane * outside_plane).sqrt()
    }
}

/// Signed distance to a wedge (triangular prism along Z axis).
pub fn sdf_wedge(p: [f32; 3], center: [f32; 3], half_width: f32, half_height: f32, half_depth: f32) -> f32 {
    let dx = p[0] - center[0];
    let dy = p[1] - center[1];
    let dz = (p[2] - center[2]).abs() - half_depth;

    // Wedge slope: from (-half_width, -half_height) to (half_width, half_height) along X
    let slope_d = dx * half_height - dy * half_width - half_width * half_height;
    let slope_len = (half_height * half_height + half_width * half_width).sqrt();
    let d_slope = slope_d / slope_len;
    let d_bottom = -dy - half_height;
    let d_z = dz;

    let inside = d_slope.max(d_bottom).max(d_z).min(0.0);
    let outside_slope = d_slope.max(0.0);
    let outside_bottom = d_bottom.max(0.0);
    let outside_z = d_z.max(0.0);
    inside + (outside_slope * outside_slope + outside_bottom * outside_bottom + outside_z * outside_z).sqrt()
}

/// Signed distance to an infinite plane defined by normal and distance from origin.
pub fn sdf_plane(p: [f32; 3], normal: [f32; 3], dist: f32) -> f32 {
    let len = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
    if len < f32::EPSILON {
        return f32::MAX;
    }
    let inv_len = 1.0 / len;
    let n = [normal[0] * inv_len, normal[1] * inv_len, normal[2] * inv_len];
    p[0] * n[0] + p[1] * n[1] + p[2] * n[2] - dist
}

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

    #[test]
    fn sdf_trace_uniforms_size() {
        assert_eq!(std::mem::size_of::<SdfTraceUniforms>(), 128);
    }

    #[test]
    fn sdf_gen_params_size() {
        assert_eq!(std::mem::size_of::<SdfGenParams>(), 32);
    }

    #[test]
    fn config_defaults_valid() {
        let config = SdfConfig::default();
        assert_eq!(config.mesh_sdf_resolution, 64);
        assert_eq!(config.global_resolution, [128, 128, 64]);
        assert_eq!(config.max_meshes, 256);
        assert!(config.max_trace_distance > 0.0);
        assert!(config.max_steps > 0);
        assert!(config.hit_threshold > 0.0);
        assert!(config.gdf_extent > 0.0);
    }

    #[test]
    fn vram_estimate_under_budget() {
        let config = SdfConfig::default();
        let estimate = SdfPass::vram_estimate(1920, 1080, &config);
        // GDF: 128*128*64*2 = 2 MB, Trace: 1920*1080*8 ~= 16 MB, Uniforms: ~192 B
        // Total should be well under 100 MB
        let budget = 100 * 1024 * 1024;
        assert!(
            estimate < budget,
            "VRAM estimate {} bytes ({:.1} MB) exceeds 100 MB budget",
            estimate,
            estimate as f64 / (1024.0 * 1024.0)
        );
    }

    #[test]
    fn sdf_default_disabled() {
        let pass = SdfPass::default();
        assert!(!pass.enabled);
        assert!(!pass.initialized);
        assert!(pass.trace_output_view().is_none());
    }

    #[test]
    fn sdf_new_enabled() {
        let pass = SdfPass::new(SdfConfig::default());
        assert!(pass.enabled);
        assert!(!pass.initialized);
    }

    // ── Analytical SDF tests ─────────────────────────────────────────────

    #[test]
    fn sphere_center_is_negative_radius() {
        let d = sdf_sphere([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], 1.0);
        assert!((d - (-1.0)).abs() < 1e-6, "Center should be -radius, got {}", d);
    }

    #[test]
    fn sphere_surface_is_zero() {
        let d = sdf_sphere([1.0, 0.0, 0.0], [0.0, 0.0, 0.0], 1.0);
        assert!(d.abs() < 1e-6, "Surface should be ~0, got {}", d);
    }

    #[test]
    fn sphere_outside_is_positive() {
        let d = sdf_sphere([3.0, 0.0, 0.0], [0.0, 0.0, 0.0], 1.0);
        assert!((d - 2.0).abs() < 1e-6, "Expected 2.0, got {}", d);
    }

    #[test]
    fn box_center_is_negative() {
        let d = sdf_box([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
        assert!(d < 0.0, "Box center should be inside (negative), got {}", d);
    }

    #[test]
    fn box_face_center_is_zero() {
        let d = sdf_box([1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
        assert!(d.abs() < 1e-6, "Face center should be ~0, got {}", d);
    }

    #[test]
    fn box_outside_is_positive() {
        let d = sdf_box([2.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
        assert!((d - 1.0).abs() < 1e-6, "Expected 1.0, got {}", d);
    }

    #[test]
    fn cylinder_center_is_negative() {
        let d = sdf_cylinder([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], 1.0, 1.0);
        assert!(d < 0.0, "Cylinder center should be inside, got {}", d);
    }

    #[test]
    fn capsule_midpoint_is_negative() {
        let d = sdf_capsule([0.0, 0.5, 0.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0.5);
        assert!(d < 0.0, "Capsule midpoint should be inside, got {}", d);
    }

    #[test]
    fn torus_ring_center_is_negative() {
        // Point on the major circle, should be inside the tube
        let d = sdf_torus([2.0, 0.0, 0.0], [0.0, 0.0, 0.0], 2.0, 0.5);
        assert!(d < 0.0, "Torus ring center should be inside, got {}", d);
    }

    #[test]
    fn plane_above_is_positive() {
        let d = sdf_plane([0.0, 5.0, 0.0], [0.0, 1.0, 0.0], 0.0);
        assert!((d - 5.0).abs() < 1e-6, "Expected 5.0, got {}", d);
    }

    #[test]
    fn plane_below_is_negative() {
        let d = sdf_plane([0.0, -3.0, 0.0], [0.0, 1.0, 0.0], 0.0);
        assert!((d - (-3.0)).abs() < 1e-6, "Expected -3.0, got {}", d);
    }

    #[test]
    fn plane_zero_normal_returns_max() {
        let d = sdf_plane([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], 0.0);
        assert_eq!(d, f32::MAX);
    }
}
