// Texture decoding and GPU upload utilities for glTF materials.
//
// Decodes PNG/JPEG images to RGBA8 and uploads to wgpu::Texture.
// Pre-allocated at import time (cold path) per CODESPEC §0.3.

/// Decoded image data ready for GPU upload.
#[derive(Debug, Clone)]
pub struct TextureData {
    pub width: u32,
    pub height: u32,
    pub rgba: Vec<u8>,
}

/// GPU-uploaded texture with sampler and bind group components.
pub struct GpuTexture {
    pub texture: wgpu::Texture,
    pub view: wgpu::TextureView,
    pub sampler: wgpu::Sampler,
    pub width: u32,
    pub height: u32,
}

/// Errors during texture decode or upload.
#[derive(Debug)]
pub enum TextureError {
    DecodeFailed(String),
    EmptyImage,
}

impl std::fmt::Display for TextureError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::DecodeFailed(msg) => write!(f, "texture_decode_failed:{}", msg),
            Self::EmptyImage => write!(f, "texture_empty:image has zero dimensions"),
        }
    }
}

impl std::error::Error for TextureError {}

/// Decode raw image bytes (PNG/JPEG) to RGBA8.
pub fn decode_image(data: &[u8]) -> Result<TextureData, TextureError> {
    let img = image::load_from_memory(data).map_err(|e| TextureError::DecodeFailed(e.to_string()))?;
    let rgba = img.to_rgba8();
    let (width, height) = rgba.dimensions();
    if width == 0 || height == 0 {
        return Err(TextureError::EmptyImage);
    }
    Ok(TextureData {
        width,
        height,
        rgba: rgba.into_raw(),
    })
}

/// Upload decoded texture data to GPU.
pub fn upload_texture(device: &wgpu::Device, queue: &wgpu::Queue, data: &TextureData, label: &str) -> GpuTexture {
    let size = wgpu::Extent3d {
        width: data.width,
        height: data.height,
        depth_or_array_layers: 1,
    };

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

    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,
    );

    let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
    let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
        label: Some(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::Nearest,
        ..Default::default()
    });

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

/// Create a 1x1 white texture (default for untextured materials).
pub fn default_white_texture(device: &wgpu::Device, queue: &wgpu::Queue) -> GpuTexture {
    let data = TextureData {
        width: 1,
        height: 1,
        rgba: vec![255, 255, 255, 255],
    };
    upload_texture(device, queue, &data, "default_white")
}

/// 1x1 flat normal texture (128,128,255,255) — default for normal maps.
pub fn default_flat_normal(device: &wgpu::Device, queue: &wgpu::Queue) -> GpuTexture {
    let data = TextureData {
        width: 1,
        height: 1,
        rgba: vec![128, 128, 255, 255],
    };
    upload_texture(device, queue, &data, "default_flat_normal")
}

/// 1x1 black texture (0,0,0,255) — default for emissive maps.
pub fn default_black(device: &wgpu::Device, queue: &wgpu::Queue) -> GpuTexture {
    let data = TextureData {
        width: 1,
        height: 1,
        rgba: vec![0, 0, 0, 255],
    };
    upload_texture(device, queue, &data, "default_black")
}

/// 1x1 roughness-metalness default (0,128,0,255) — metallic=0, roughness=0.5.
/// Blue channel = metallic, Green channel = roughness (glTF convention).
pub fn default_roughness_metalness(device: &wgpu::Device, queue: &wgpu::Queue) -> GpuTexture {
    let data = TextureData {
        width: 1,
        height: 1,
        rgba: vec![0, 128, 0, 255],
    };
    upload_texture(device, queue, &data, "default_roughness_metalness")
}

/// 1x1 white AO texture (255,255,255,255) — AO=1.0 (no occlusion).
pub fn default_white_ao(device: &wgpu::Device, queue: &wgpu::Queue) -> GpuTexture {
    let data = TextureData {
        width: 1,
        height: 1,
        rgba: vec![255, 255, 255, 255],
    };
    upload_texture(device, queue, &data, "default_white_ao")
}

impl GpuTexture {
    pub fn destroy(&mut self) {
        self.texture.destroy();
    }
}

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

    #[test]
    fn decode_minimal_png() {
        // 1x1 red PNG (67 bytes)
        let png_bytes = {
            let mut buf = Vec::new();
            let encoder = image::codecs::png::PngEncoder::new(&mut buf);
            use image::ImageEncoder;
            encoder
                .write_image(&[255, 0, 0, 255], 1, 1, image::ExtendedColorType::Rgba8)
                .unwrap();
            buf
        };
        let data = decode_image(&png_bytes).expect("decode failed");
        assert_eq!(data.width, 1);
        assert_eq!(data.height, 1);
        assert_eq!(data.rgba.len(), 4);
        assert_eq!(data.rgba[0], 255); // red
        assert_eq!(data.rgba[3], 255); // alpha
    }

    #[test]
    fn decode_invalid_data() {
        let result = decode_image(&[0, 1, 2, 3]);
        assert!(result.is_err());
    }

    #[test]
    fn default_flat_normal_data() {
        let data = TextureData {
            width: 1,
            height: 1,
            rgba: vec![128, 128, 255, 255],
        };
        assert_eq!(data.rgba, [128, 128, 255, 255]);
    }

    #[test]
    fn default_black_data() {
        let data = TextureData {
            width: 1,
            height: 1,
            rgba: vec![0, 0, 0, 255],
        };
        assert_eq!(data.rgba, [0, 0, 0, 255]);
    }

    #[test]
    fn default_roughness_metalness_data() {
        let data = TextureData {
            width: 1,
            height: 1,
            rgba: vec![0, 128, 0, 255],
        };
        assert_eq!(data.rgba, [0, 128, 0, 255]);
    }

    #[test]
    fn texture_data_size() {
        let data = TextureData {
            width: 4,
            height: 4,
            rgba: vec![0u8; 64],
        };
        assert_eq!(data.rgba.len(), (data.width * data.height * 4) as usize);
    }
}
