//! DreamSurface — unified material + texture pipeline for PBR.
//!
//! Loads all 5 glTF PBR texture slots (base_color, normal, roughness_metalness,
//! occlusion, emissive) with correct format (sRGB vs linear) and full mip chains.
//! Pre-allocates per-material bind groups at import time. Zero per-frame allocation.
//!
//! Clean Compute: all texture upload and bind group creation happens at import time.
//! Per-frame work is zero — render_pbr() just sets the pre-built bind group.

use crate::texture::{GpuTexture, TextureData};
use crate::vram::VramTracker;

// ── Texture type classification ──────────────────────────────────────

/// PBR texture slot type. Determines GPU format (sRGB vs linear) and label.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SurfaceTextureType {
    /// Base color / albedo. sRGB gamma space.
    BaseColor,
    /// Normal map. Linear space.
    Normal,
    /// Roughness (G) + Metalness (B). Linear space.
    RoughnessMetalness,
    /// Ambient occlusion (R channel only). Linear space.
    Occlusion,
    /// Emissive color. sRGB gamma space.
    Emissive,
}

impl SurfaceTextureType {
    /// GPU texture format for this slot (sRGB for color, linear for data).
    pub fn format(self) -> wgpu::TextureFormat {
        match self {
            Self::BaseColor | Self::Emissive => wgpu::TextureFormat::Rgba8UnormSrgb,
            Self::Normal | Self::RoughnessMetalness | Self::Occlusion => wgpu::TextureFormat::Rgba8Unorm,
        }
    }

    /// Label suffix for GPU resource naming.
    pub fn label_suffix(self) -> &'static str {
        match self {
            Self::BaseColor => "base_color",
            Self::Normal => "normal",
            Self::RoughnessMetalness => "roughness_metalness",
            Self::Occlusion => "occlusion",
            Self::Emissive => "emissive",
        }
    }
}

// ── Mip chain generation ─────────────────────────────────────────────

/// Compute the full mip chain level count for the given dimensions.
/// Returns floor(log2(max(w, h))) + 1.
pub fn mip_level_count(width: u32, height: u32) -> u32 {
    let max_dim = width.max(height);
    if max_dim == 0 {
        return 1;
    }
    (32 - max_dim.leading_zeros()).max(1)
}

/// Box-filter downsample RGBA8 image by 2x in each dimension.
/// Averages each 2x2 block. Handles odd dimensions by clamping source reads.
fn box_filter_downsample(src: &[u8], src_w: u32, src_h: u32) -> (Vec<u8>, u32, u32) {
    let dst_w = (src_w / 2).max(1);
    let dst_h = (src_h / 2).max(1);
    let mut dst = vec![0u8; (dst_w * dst_h * 4) as usize];

    for y in 0..dst_h {
        for x in 0..dst_w {
            // Source pixel coordinates (clamped to valid range)
            let sx = (x * 2).min(src_w - 1);
            let sy = (y * 2).min(src_h - 1);
            let sx1 = (sx + 1).min(src_w - 1);
            let sy1 = (sy + 1).min(src_h - 1);

            let stride = src_w * 4;
            let i00 = (sy * stride + sx * 4) as usize;
            let i10 = (sy * stride + sx1 * 4) as usize;
            let i01 = (sy1 * stride + sx * 4) as usize;
            let i11 = (sy1 * stride + sx1 * 4) as usize;

            let dst_idx = (y * dst_w * 4 + x * 4) as usize;
            for c in 0..4 {
                let sum = src[i00 + c] as u32 + src[i10 + c] as u32 + src[i01 + c] as u32 + src[i11 + c] as u32;
                dst[dst_idx + c] = (sum / 4) as u8;
            }
        }
    }

    (dst, dst_w, dst_h)
}

// ── Texture upload with mip chain ────────────────────────────────────

/// Upload a texture with correct format and CPU-generated mip chain.
///
/// Mip levels: `floor(log2(max(w,h))) + 1`. Box-filter downsample per level.
/// Returns `None` if data is empty or VRAM budget exceeded.
///
/// CODESPEC §0.3: cold-path allocation only. No per-frame work.
pub fn upload_surface_texture(
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    data: &TextureData,
    tex_type: SurfaceTextureType,
    label: &str,
    vram: Option<&mut VramTracker>,
) -> Option<GpuTexture> {
    if data.rgba.is_empty() || data.width == 0 || data.height == 0 {
        return None;
    }

    let format = tex_type.format();
    let mip_count = mip_level_count(data.width, data.height);
    let full_label = format!("{}_{}", label, tex_type.label_suffix());

    // Estimate VRAM: sum of all mip levels (4 bytes per pixel for RGBA8)
    let mut vram_bytes: u64 = 0;
    {
        let mut w = data.width;
        let mut h = data.height;
        for _ in 0..mip_count {
            vram_bytes += (w as u64) * (h as u64) * 4;
            w = (w / 2).max(1);
            h = (h / 2).max(1);
        }
    }

    if let Some(tracker) = vram {
        if !tracker.try_allocate(&full_label, vram_bytes) {
            return None;
        }
    }

    let size = wgpu::Extent3d {
        width: data.width,
        height: data.height,
        depth_or_array_layers: 1,
    };

    let texture = device.create_texture(&wgpu::TextureDescriptor {
        label: Some(&full_label),
        size,
        mip_level_count: mip_count,
        sample_count: 1,
        dimension: wgpu::TextureDimension::D2,
        format,
        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
        view_formats: &[],
    });

    // Upload mip 0
    queue.write_texture(
        wgpu::TexelCopyTextureInfo {
            texture: &texture,
            mip_level: 0,
            origin: wgpu::Origin3d::ZERO,
            aspect: wgpu::TextureAspect::All,
        },
        &data.rgba,
        wgpu::TexelCopyBufferLayout {
            offset: 0,
            bytes_per_row: Some(4 * data.width),
            rows_per_image: Some(data.height),
        },
        size,
    );

    // Generate and upload subsequent mip levels via CPU box filter
    let mut prev_data = data.rgba.clone();
    let mut prev_w = data.width;
    let mut prev_h = data.height;

    for level in 1..mip_count {
        let (mip_data, mip_w, mip_h) = box_filter_downsample(&prev_data, prev_w, prev_h);

        queue.write_texture(
            wgpu::TexelCopyTextureInfo {
                texture: &texture,
                mip_level: level,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            &mip_data,
            wgpu::TexelCopyBufferLayout {
                offset: 0,
                bytes_per_row: Some(4 * mip_w),
                rows_per_image: Some(mip_h),
            },
            wgpu::Extent3d {
                width: mip_w,
                height: mip_h,
                depth_or_array_layers: 1,
            },
        );

        prev_data = mip_data;
        prev_w = mip_w;
        prev_h = mip_h;
    }

    // View spanning all mip levels
    let view = texture.create_view(&wgpu::TextureViewDescriptor {
        label: Some(&full_label),
        ..Default::default()
    });

    // Trilinear sampler for mip chain
    let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
        label: Some(&full_label),
        address_mode_u: wgpu::AddressMode::Repeat,
        address_mode_v: wgpu::AddressMode::Repeat,
        address_mode_w: wgpu::AddressMode::ClampToEdge,
        mag_filter: wgpu::FilterMode::Linear,
        min_filter: wgpu::FilterMode::Linear,
        mipmap_filter: wgpu::MipmapFilterMode::Linear,
        ..Default::default()
    });

    Some(GpuTexture {
        texture,
        view,
        sampler,
        width: data.width,
        height: data.height,
    })
}

// ── Default textures ─────────────────────────────────────────────────

/// Pre-allocated 1x1 fallback textures for missing PBR material slots.
///
/// Created once at init. Referenced by every `SurfaceMaterial` that lacks
/// a texture in a given slot. Avoids branching or optional bind group entries.
pub struct PbrDefaultTextures {
    /// 1x1 white (255,255,255,255) — base_color fallback.
    pub white: GpuTexture,
    /// 1x1 flat normal (128,128,255,255) — normal fallback.
    pub flat_normal: GpuTexture,
    /// 1x1 black (0,0,0,255) — emissive fallback.
    pub black: GpuTexture,
    /// 1x1 default ORM (0,128,0,255) — roughness=0.5, metallic=0 fallback.
    pub default_orm: GpuTexture,
    /// 1x1 white (255,255,255,255) — AO=1.0 (no occlusion) fallback.
    pub white_ao: GpuTexture,
    /// Shared sampler for all default textures.
    pub sampler: wgpu::Sampler,
}

impl PbrDefaultTextures {
    /// Create all fallback textures. Called once at renderer init.
    pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
        use crate::texture;

        let white = texture::default_white_texture(device, queue);
        let flat_normal = texture::default_flat_normal(device, queue);
        let black = texture::default_black(device, queue);
        let default_orm = texture::default_roughness_metalness(device, queue);
        let white_ao = texture::default_white_ao(device, queue);

        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("pbr_default_sampler"),
            address_mode_u: wgpu::AddressMode::Repeat,
            address_mode_v: wgpu::AddressMode::Repeat,
            address_mode_w: wgpu::AddressMode::ClampToEdge,
            mag_filter: wgpu::FilterMode::Linear,
            min_filter: wgpu::FilterMode::Linear,
            mipmap_filter: wgpu::MipmapFilterMode::Linear,
            ..Default::default()
        });

        Self {
            white,
            flat_normal,
            black,
            default_orm,
            white_ao,
            sampler,
        }
    }
}

// ── Surface material ─────────────────────────────────────────────────

/// A complete set of GPU-side PBR surface textures for one material.
///
/// Pre-built at import time. `bind_group` matches `@group(1)` in `pbr.wgsl`:
/// - binding 0: base_color (texture_2d)
/// - binding 1: normal (texture_2d)
/// - binding 2: roughness_metalness (texture_2d)
/// - binding 3: occlusion (texture_2d)
/// - binding 4: emissive (texture_2d)
/// - binding 5: sampler
pub struct SurfaceMaterial {
    pub base_color: Option<GpuTexture>,
    pub normal: Option<GpuTexture>,
    pub roughness_metalness: Option<GpuTexture>,
    pub occlusion: Option<GpuTexture>,
    pub emissive: Option<GpuTexture>,
    pub bind_group: wgpu::BindGroup,
    /// Flags for the uniform `has_*_tex` fields in PbrUniforms.
    pub has_base_color: bool,
    pub has_normal: bool,
    pub has_roughness_metalness: bool,
    pub has_occlusion: bool,
    pub has_emissive: bool,
    // Material factors (copied from GltfMaterial for uniform upload)
    pub base_color_factor: [f32; 4],
    pub metallic_factor: f32,
    pub roughness_factor: f32,
    pub emissive_factor: [f32; 3],
    pub normal_scale: f32,
    pub occlusion_strength: f32,
}

impl SurfaceMaterial {
    /// Create from glTF material data, uploading all available textures.
    ///
    /// Uses `defaults` fallback for any missing texture slot so that the
    /// bind group always has all 5 textures + 1 sampler bound.
    ///
    /// CODESPEC §0.3: all GPU allocation happens here (cold path).
    pub fn from_gltf(
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        mat: &crate::gltf::GltfMaterial,
        defaults: &PbrDefaultTextures,
        bgl: &wgpu::BindGroupLayout,
    ) -> Self {
        // Upload base_color if present (the only texture currently in GltfMaterial)
        let base_color = mat.base_color_texture.as_ref().and_then(|tex_data| {
            upload_surface_texture(device, queue, tex_data, SurfaceTextureType::BaseColor, &mat.name, None)
        });

        // Upload all 5 PBR texture slots from GltfMaterial.
        let normal = mat.normal_texture.as_ref().and_then(|tex_data| {
            upload_surface_texture(device, queue, tex_data, SurfaceTextureType::Normal, &mat.name, None)
        });
        let roughness_metalness = mat.roughness_metalness_texture.as_ref().and_then(|tex_data| {
            upload_surface_texture(
                device,
                queue,
                tex_data,
                SurfaceTextureType::RoughnessMetalness,
                &mat.name,
                None,
            )
        });
        let occlusion = mat.occlusion_texture.as_ref().and_then(|tex_data| {
            upload_surface_texture(device, queue, tex_data, SurfaceTextureType::Occlusion, &mat.name, None)
        });
        let emissive = mat.emissive_texture.as_ref().and_then(|tex_data| {
            upload_surface_texture(device, queue, tex_data, SurfaceTextureType::Emissive, &mat.name, None)
        });

        let has_base_color = base_color.is_some();
        let has_normal = normal.is_some();
        let has_roughness_metalness = roughness_metalness.is_some();
        let has_occlusion = occlusion.is_some();
        let has_emissive = emissive.is_some();

        // Select view for each slot: uploaded texture or fallback default
        let bc_view = base_color.as_ref().map(|t| &t.view).unwrap_or(&defaults.white.view);
        let nm_view = normal.as_ref().map(|t| &t.view).unwrap_or(&defaults.flat_normal.view);
        let rm_view = roughness_metalness
            .as_ref()
            .map(|t| &t.view)
            .unwrap_or(&defaults.default_orm.view);
        let ao_view = occlusion.as_ref().map(|t| &t.view).unwrap_or(&defaults.white_ao.view);
        let em_view = emissive.as_ref().map(|t| &t.view).unwrap_or(&defaults.black.view);

        // Use the uploaded texture's sampler if available, otherwise the default
        let sampler_ref = base_color.as_ref().map(|t| &t.sampler).unwrap_or(&defaults.sampler);

        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some(&format!("surface_material_{}", mat.name)),
            layout: bgl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(bc_view),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::TextureView(nm_view),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: wgpu::BindingResource::TextureView(rm_view),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: wgpu::BindingResource::TextureView(ao_view),
                },
                wgpu::BindGroupEntry {
                    binding: 4,
                    resource: wgpu::BindingResource::TextureView(em_view),
                },
                wgpu::BindGroupEntry {
                    binding: 5,
                    resource: wgpu::BindingResource::Sampler(sampler_ref),
                },
            ],
        });

        Self {
            base_color,
            normal,
            roughness_metalness,
            occlusion,
            emissive,
            bind_group,
            has_base_color,
            has_normal,
            has_roughness_metalness,
            has_occlusion,
            has_emissive,
            base_color_factor: mat.base_color_factor,
            metallic_factor: mat.metallic_factor,
            roughness_factor: mat.roughness_factor,
            emissive_factor: mat.emissive_factor,
            normal_scale: mat.normal_scale,
            occlusion_strength: mat.occlusion_strength,
        }
    }

    /// Create a default material using only fallback textures.
    ///
    /// Useful for primitives and objects that have no material assigned.
    pub fn default_material(device: &wgpu::Device, defaults: &PbrDefaultTextures, bgl: &wgpu::BindGroupLayout) -> Self {
        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("surface_material_default"),
            layout: bgl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(&defaults.white.view),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::TextureView(&defaults.flat_normal.view),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: wgpu::BindingResource::TextureView(&defaults.default_orm.view),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: wgpu::BindingResource::TextureView(&defaults.white_ao.view),
                },
                wgpu::BindGroupEntry {
                    binding: 4,
                    resource: wgpu::BindingResource::TextureView(&defaults.black.view),
                },
                wgpu::BindGroupEntry {
                    binding: 5,
                    resource: wgpu::BindingResource::Sampler(&defaults.sampler),
                },
            ],
        });

        Self {
            base_color: None,
            normal: None,
            roughness_metalness: None,
            occlusion: None,
            emissive: None,
            bind_group,
            has_base_color: false,
            has_normal: false,
            has_roughness_metalness: false,
            has_occlusion: false,
            has_emissive: false,
            base_color_factor: [1.0, 1.0, 1.0, 1.0],
            metallic_factor: 0.0,
            roughness_factor: 1.0,
            emissive_factor: [0.0, 0.0, 0.0],
            normal_scale: 1.0,
            occlusion_strength: 1.0,
        }
    }
}

// ── Tests ────────────────────────────────────────────────────────────

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

    // ── SurfaceTextureType ───────────────────────────────────────

    #[test]
    fn base_color_is_srgb() {
        assert_eq!(
            SurfaceTextureType::BaseColor.format(),
            wgpu::TextureFormat::Rgba8UnormSrgb,
        );
    }

    #[test]
    fn emissive_is_srgb() {
        assert_eq!(
            SurfaceTextureType::Emissive.format(),
            wgpu::TextureFormat::Rgba8UnormSrgb,
        );
    }

    #[test]
    fn normal_is_linear() {
        assert_eq!(SurfaceTextureType::Normal.format(), wgpu::TextureFormat::Rgba8Unorm,);
    }

    #[test]
    fn roughness_metalness_is_linear() {
        assert_eq!(
            SurfaceTextureType::RoughnessMetalness.format(),
            wgpu::TextureFormat::Rgba8Unorm,
        );
    }

    #[test]
    fn occlusion_is_linear() {
        assert_eq!(SurfaceTextureType::Occlusion.format(), wgpu::TextureFormat::Rgba8Unorm,);
    }

    #[test]
    fn label_suffixes_unique() {
        let types = [
            SurfaceTextureType::BaseColor,
            SurfaceTextureType::Normal,
            SurfaceTextureType::RoughnessMetalness,
            SurfaceTextureType::Occlusion,
            SurfaceTextureType::Emissive,
        ];
        let labels: Vec<&str> = types.iter().map(|t| t.label_suffix()).collect();
        for (i, a) in labels.iter().enumerate() {
            for (j, b) in labels.iter().enumerate() {
                if i != j {
                    assert_ne!(a, b, "duplicate label suffix");
                }
            }
        }
    }

    // ── Box filter downsample ────────────────────────────────────

    #[test]
    fn downsample_4x4_to_2x2() {
        // 4x4 image: top-left quadrant red, rest green
        let mut src = vec![0u8; 4 * 4 * 4];
        // Row 0: red, red, green, green
        // Row 1: red, red, green, green
        // Row 2: green, green, green, green
        // Row 3: green, green, green, green
        for y in 0..4u32 {
            for x in 0..4u32 {
                let idx = (y * 4 + x) as usize * 4;
                if x < 2 && y < 2 {
                    src[idx] = 255; // R
                    src[idx + 1] = 0; // G
                    src[idx + 2] = 0; // B
                    src[idx + 3] = 255; // A
                } else {
                    src[idx] = 0;
                    src[idx + 1] = 255;
                    src[idx + 2] = 0;
                    src[idx + 3] = 255;
                }
            }
        }

        let (dst, w, h) = box_filter_downsample(&src, 4, 4);
        assert_eq!(w, 2);
        assert_eq!(h, 2);
        assert_eq!(dst.len(), 2 * 2 * 4);

        // Top-left pixel: average of 4 red pixels → red
        assert_eq!(dst[0], 255); // R
        assert_eq!(dst[1], 0); // G
        assert_eq!(dst[2], 0); // B
        assert_eq!(dst[3], 255); // A

        // Top-right pixel: average of 4 green pixels → green
        assert_eq!(dst[4], 0);
        assert_eq!(dst[5], 255);
        assert_eq!(dst[6], 0);
        assert_eq!(dst[7], 255);
    }

    #[test]
    fn downsample_2x2_to_1x1() {
        // 2x2: (100,0,0,255), (200,0,0,255), (0,100,0,255), (0,200,0,255)
        let src = vec![100, 0, 0, 255, 200, 0, 0, 255, 0, 100, 0, 255, 0, 200, 0, 255];

        let (dst, w, h) = box_filter_downsample(&src, 2, 2);
        assert_eq!(w, 1);
        assert_eq!(h, 1);
        assert_eq!(dst.len(), 4);

        // Average: R=(100+200+0+0)/4=75, G=(0+0+100+200)/4=75, B=0, A=255
        assert_eq!(dst[0], 75);
        assert_eq!(dst[1], 75);
        assert_eq!(dst[2], 0);
        assert_eq!(dst[3], 255);
    }

    #[test]
    fn downsample_1x1_stays_1x1() {
        let src = vec![128, 64, 32, 255];
        let (dst, w, h) = box_filter_downsample(&src, 1, 1);
        assert_eq!(w, 1);
        assert_eq!(h, 1);
        // All four samples clamp to the same pixel
        assert_eq!(dst, vec![128, 64, 32, 255]);
    }

    #[test]
    fn downsample_odd_dimensions() {
        // 3x3 → 1x1 (3/2 = 1)
        let src = vec![0u8; 3 * 3 * 4];
        let (_, w, h) = box_filter_downsample(&src, 3, 3);
        assert_eq!(w, 1);
        assert_eq!(h, 1);
    }

    // ── Mip level count ──────────────────────────────────────────

    #[test]
    fn mip_count_1x1() {
        assert_eq!(mip_level_count(1, 1), 1);
    }

    #[test]
    fn mip_count_2x2() {
        // log2(2) + 1 = 2
        assert_eq!(mip_level_count(2, 2), 2);
    }

    #[test]
    fn mip_count_4x4() {
        // log2(4) + 1 = 3
        assert_eq!(mip_level_count(4, 4), 3);
    }

    #[test]
    fn mip_count_256x256() {
        // log2(256) + 1 = 9
        assert_eq!(mip_level_count(256, 256), 9);
    }

    #[test]
    fn mip_count_1024x512() {
        // log2(1024) + 1 = 11
        assert_eq!(mip_level_count(1024, 512), 11);
    }

    #[test]
    fn mip_count_non_power_of_two() {
        // 300: log2(300) = 8.22 → floor = 8 → 8 + 1 = 9
        assert_eq!(mip_level_count(300, 200), 9);
    }

    #[test]
    fn mip_count_zero_dimension() {
        assert_eq!(mip_level_count(0, 0), 1);
        assert_eq!(mip_level_count(0, 256), 9);
        assert_eq!(mip_level_count(256, 0), 9);
    }

    // ── SurfaceTextureType equality ──────────────────────────────

    #[test]
    fn texture_type_eq() {
        assert_eq!(SurfaceTextureType::BaseColor, SurfaceTextureType::BaseColor);
        assert_ne!(SurfaceTextureType::BaseColor, SurfaceTextureType::Normal);
    }

    #[test]
    fn texture_type_clone() {
        let t = SurfaceTextureType::Emissive;
        let t2 = t;
        assert_eq!(t, t2);
    }

    #[test]
    fn texture_type_debug() {
        let s = format!("{:?}", SurfaceTextureType::RoughnessMetalness);
        assert!(s.contains("RoughnessMetalness"));
    }

    // ── upload_surface_texture rejects empty data ────────────────

    // NOTE: upload_surface_texture and SurfaceMaterial require a GPU device,
    // so they are tested via the gpu_device_tests integration test or manually.
    // These unit tests verify the non-GPU logic exhaustively.

    #[test]
    fn upload_returns_none_for_empty_data() {
        // We cannot call upload_surface_texture without a device, but we can
        // verify the guard logic by checking that empty TextureData would
        // cause early return. The function checks data.rgba.is_empty().
        let empty = TextureData {
            width: 0,
            height: 0,
            rgba: vec![],
        };
        assert!(empty.rgba.is_empty());
        assert_eq!(empty.width, 0);
    }
}
