//! Texture loading and processing

use image::ImageReader;
use image::ImageFormat;
use std::path::Path;
use thiserror::Error;

/// Error type for texture loading operations
#[derive(Error, Debug)]
pub enum TextureError {
    /// Image decoding error
    #[error("Image decoding error: {0}")]
    DecodeError(String),

    /// Unsupported texture format
    #[error("Unsupported format: {0}")]
    UnsupportedFormat(String),

    /// IO error during texture loading
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

/// Represents a loaded texture
#[derive(Debug, Clone)]
pub struct Texture {
    /// Width of texture
    pub width: u32,
    /// Height of texture
    pub height: u32,
    /// Raw texture data
    pub data: Vec<u8>,
    /// Format of the texture data
    pub format: TextureFormat,
}

/// Supported texture formats
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TextureFormat {
    /// Standard RGBA 8-bit per channel
    Rgba8,
    /// Standard RGB 8-bit per channel
    Rgb8,
    /// WebP compressed format
    WebP,
    /// KTX2 with ETC1S compression (efficient for distribution)
    Ktx2Etc1s,
    /// KTX2 with UASTC compression (high quality, transcodable)
    Ktx2Uastc,
}

impl TextureFormat {
    /// Get the default file extension for this format
    pub fn extension(&self) -> &'static str {
        match self {
            Self::Rgba8 | Self::Rgb8 => "png",
            Self::WebP => "webp",
            Self::Ktx2Etc1s | Self::Ktx2Uastc => "ktx2",
        }
    }
}

/// Loads and processes texture files
#[derive(Default, Clone)]
pub struct TextureLoader;

impl TextureLoader {
    /// Create a new texture loader
    pub fn new() -> Self {
        Self
    }

    /// Load a texture from binary data
    pub fn load(&self, data: &[u8]) -> Result<Texture, TextureError> {
        let format =
            image::guess_format(data).map_err(|e| TextureError::DecodeError(e.to_string()))?;

        match format {
            ImageFormat::Jpeg | ImageFormat::Png | ImageFormat::WebP => {}
            _ => {
                return Err(TextureError::UnsupportedFormat(format!(
                    "Only JPG, PNG and WebP formats are supported, got {:?}",
                    format.extensions_str()
                )))
            }
        }

        let img = ImageReader::with_format(std::io::Cursor::new(data), format)
            .decode()
            .map_err(|e| TextureError::DecodeError(e.to_string()))?;

        let rgba_img = img.into_rgba8();
        let (width, height) = rgba_img.dimensions();

        Ok(Texture {
            width,
            height,
            data: rgba_img.into_raw(),
            format: if format == ImageFormat::WebP {
                TextureFormat::WebP
            } else {
                TextureFormat::Rgba8
            },
        })
    }

    /// Load a texture from a file synchronously
    pub fn load_file_sync<P: AsRef<Path>>(&self, path: P) -> Result<Texture, TextureError> {
        let data = std::fs::read(path)?;
        self.load(&data)
    }
}

// Async loading (requires tokio)
#[cfg(feature = "runtime-tokio")]
impl TextureLoader {
    /// Load a texture from a file asynchronously
    pub async fn load_file<P: AsRef<Path>>(&self, path: P) -> Result<Texture, TextureError> {
        let data = tokio::fs::read(path).await?;
        self.load(&data)
    }

    /// Load a texture with a specific format preference
    pub async fn load_file_formatted<P: AsRef<Path>>(
        &self,
        path: P,
        format: TextureFormat,
    ) -> Result<Texture, TextureError> {
        let path = path.as_ref();
        let extension = format.extension();
        let formatted_path = path.with_extension(extension);

        // Try to load the formatted file
        if tokio::fs::metadata(&formatted_path).await.is_ok() {
            self.load_file(formatted_path).await
        } else {
            self.load_file(path).await
        }
    }
}

// Fallback loading (for tests/mock runtime)
#[cfg(not(feature = "runtime-tokio"))]
impl TextureLoader {
    /// Load a texture from a file (synchronous fallback)
    pub async fn load_file<P: AsRef<Path>>(&self, path: P) -> Result<Texture, TextureError> {
        let data = std::fs::read(path)?;
        self.load(&data)
    }

    /// Load a texture with a specific format preference
    pub async fn load_file_formatted<P: AsRef<Path>>(
        &self,
        path: P,
        format: TextureFormat,
    ) -> Result<Texture, TextureError> {
        let path = path.as_ref();
        let extension = format.extension();
        let formatted_path = path.with_extension(extension);

        // Try to load the formatted file
        if formatted_path.exists() {
            self.load_file(formatted_path).await
        } else {
            self.load_file(path).await
        }
    }
}

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

    #[test]
    fn test_load_jpeg_sync() {
        let loader = TextureLoader::new();

        let mut img = image::RgbaImage::new(1, 1);
        img.put_pixel(0, 0, image::Rgba([255, 255, 255, 255]));

        let mut jpeg_data = Vec::new();
        img.write_to(
            &mut std::io::Cursor::new(&mut jpeg_data),
            image::ImageFormat::Jpeg,
        )
        .expect("Failed to encode test image");

        let result = loader.load(&jpeg_data);
        assert!(result.is_ok(), "Failed to load JPEG: {:?}", result.err());

        let texture = result.unwrap();
        assert_eq!(texture.width, 1);
        assert_eq!(texture.height, 1);
        assert_eq!(texture.format, TextureFormat::Rgba8);
    }

    #[test]
    fn test_load_png_sync() {
        let loader = TextureLoader::new();

        let mut img = image::RgbaImage::new(1, 1);
        img.put_pixel(0, 0, image::Rgba([255, 0, 0, 255]));

        let mut png_data = Vec::new();
        img.write_to(
            &mut std::io::Cursor::new(&mut png_data),
            image::ImageFormat::Png,
        )
        .expect("Failed to encode test image");

        let result = loader.load(&png_data);
        assert!(result.is_ok(), "Failed to load PNG: {:?}", result.err());

        let texture = result.unwrap();
        assert_eq!(texture.width, 1);
        assert_eq!(texture.height, 1);
    }

    #[test]
    fn test_load_webp_sync() {
        let loader = TextureLoader::new();

        let mut img = image::RgbaImage::new(1, 1);
        img.put_pixel(0, 0, image::Rgba([0, 255, 0, 255]));

        let mut webp_data = Vec::new();
        // Use WebP encoder
        let mut cursor = std::io::Cursor::new(&mut webp_data);
        let encoder = image::codecs::webp::WebPEncoder::new_lossless(&mut cursor);
        encoder
            .encode(&img, 1, 1, image::ExtendedColorType::Rgba8)
            .expect("Failed to encode WebP");

        let result = loader.load(&webp_data);
        assert!(result.is_ok(), "Failed to load WebP: {:?}", result.err());

        let texture = result.unwrap();
        assert_eq!(texture.width, 1);
        assert_eq!(texture.height, 1);
        assert_eq!(texture.format, TextureFormat::WebP);
    }
}
