//! Tier 1 codec: delegates to the `image` crate for all common raster formats.
//!
//! The `image` crate handles PNG (incl. APNG), JPEG, GIF (incl. animation),
//! WebP, BMP, ICO/CUR, and AVIF (when the `avif` feature is enabled, which
//! it is in our `Cargo.toml`).
//!
//! This codec is intentionally thin — it just normalizes the `image` crate's
//! `DynamicImage` into our `DecodedImage` type so the rest of the codebase
//! doesn't depend on `image` crate types directly.

use image::ImageReader;

use super::{Codec, DecodeError, DecodedImage};

pub struct ImageCrateCodec;

impl Codec for ImageCrateCodec {
    fn name(&self) -> &'static str {
        "image-crate"
    }

    fn extensions(&self) -> &'static [&'static str] {
        // Keep this list in sync with the `image` crate features in Cargo.toml.
        &[
            "png", "apng",            // PNG (incl. animated)
            "jpg", "jpeg", "jfif",    // JPEG
            "gif",                    // GIF (animated)
            "webp",                   // WebP (lossy + lossless + animated)
            "bmp",                    // Windows bitmap
            "ico", "cur",             // Windows icon / cursor
            "avif",                   // AV1 image file format
        ]
    }

    fn decode(&self, bytes: &[u8]) -> Result<DecodedImage, DecodeError> {
        let reader = ImageReader::new(std::io::Cursor::new(bytes))
            .with_guessed_format()
            .map_err(|e| DecodeError::Decode(e.to_string()))?;

        let format = reader
            .format()
            .map(|f| f.extensions_str().first().copied().unwrap_or("unknown"))
            .unwrap_or("unknown");

        let format_name: &'static str = match format {
            "png" | "apng" => "PNG",
            "jpg" | "jpeg" | "jfif" => "JPEG",
            "gif" => "GIF",
            "webp" => "WebP",
            "bmp" => "BMP",
            "ico" => "ICO",
            "cur" => "CUR",
            "avif" => "AVIF",
            _ => "Unknown",
        };

        let img = reader
            .decode()
            .map_err(|e| DecodeError::Decode(e.to_string()))?
            .to_rgba8();

        let width = img.width();
        let height = img.height();
        let pixels = img.into_raw();

        Ok(DecodedImage {
            width,
            height,
            pixels,
            format: format_name,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use image::{ImageBuffer, Rgba, ImageEncoder, ExtendedColorType};
    use image::codecs::png::PngEncoder;

    #[test]
    fn decodes_png_from_memory() {
        // Build a 2x2 RGBA PNG in memory.
        let img: ImageBuffer<Rgba<u8>, Vec<u8>> = ImageBuffer::from_fn(2, 2, |x, y| {
            if (x + y) % 2 == 0 {
                Rgba([255, 0, 0, 255])
            } else {
                Rgba([0, 255, 0, 255])
            }
        });
        let mut bytes = Vec::new();
        PngEncoder::new(&mut bytes)
            .write_image(img.as_raw(), 2, 2, ExtendedColorType::Rgba8)
            .unwrap();

        let codec = ImageCrateCodec;
        let decoded = codec.decode(&bytes).unwrap();
        assert_eq!(decoded.width, 2);
        assert_eq!(decoded.height, 2);
        assert_eq!(decoded.format, "PNG");
        assert_eq!(decoded.pixels.len(), 16); // 2*2*4
    }

    #[test]
    fn rejects_garbage_bytes() {
        let codec = ImageCrateCodec;
        let result = codec.decode(&[0xff, 0xfe, 0xfd]);
        assert!(matches!(result, Err(DecodeError::Decode(_))));
    }
}
