use std::path::Path;

/// An image decoded to tightly-packed RGBA8 (`rgba.len() == width * height * 4`).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DecodedImage {
    pub width: u32,
    pub height: u32,
    pub rgba: Vec<u8>,
}

/// Error returned when an image cannot be read or decoded.
#[derive(thiserror::Error, Debug)]
pub enum DecodeError {
    #[error("failed to decode image {path}: {source}")]
    Image {
        path: String,
        #[source]
        source: image::ImageError,
    },
}

/// Read just an image's pixel dimensions from its header, without decoding the
/// pixel data. Used to estimate decoded memory cost before committing to a load.
pub fn dimensions(path: &Path) -> Result<(u32, u32), DecodeError> {
    image::image_dimensions(path).map_err(|source| DecodeError::Image {
        path: path.display().to_string(),
        source,
    })
}

/// Decode an image file to RGBA8.
pub fn load(path: &Path) -> Result<DecodedImage, DecodeError> {
    let dynimg = image::open(path).map_err(|source| DecodeError::Image {
        path: path.display().to_string(),
        source,
    })?;
    let rgba = dynimg.to_rgba8();
    let (width, height) = rgba.dimensions();
    Ok(DecodedImage {
        width,
        height,
        rgba: rgba.into_raw(),
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use image::{Rgba, RgbaImage};

    fn write_png(dir: &std::path::Path, name: &str, w: u32, h: u32) -> std::path::PathBuf {
        let mut img = RgbaImage::new(w, h);
        for p in img.pixels_mut() {
            *p = Rgba([10, 20, 30, 255]);
        }
        let path = dir.join(name);
        img.save(&path).unwrap();
        path
    }

    #[test]
    fn loads_png_dimensions_and_rgba() {
        let dir = tempfile::tempdir().unwrap();
        let path = write_png(dir.path(), "a.png", 4, 3);

        let img = load(&path).unwrap();

        assert_eq!(img.width, 4);
        assert_eq!(img.height, 3);
        assert_eq!(img.rgba.len(), 4 * 3 * 4);
        assert_eq!(&img.rgba[0..4], &[10, 20, 30, 255]);
    }

    #[test]
    fn missing_file_is_error() {
        let err = load(std::path::Path::new("/nope/missing.png"));
        assert!(err.is_err());
    }

    #[test]
    fn dimensions_reads_size_without_full_decode() {
        let dir = tempfile::tempdir().unwrap();
        let path = write_png(dir.path(), "dims.png", 7, 5);
        assert_eq!(dimensions(&path).unwrap(), (7, 5));
    }

    #[test]
    fn dimensions_errors_on_non_image() {
        let dir = tempfile::tempdir().unwrap();
        let bad = dir.path().join("bad.png");
        std::fs::write(&bad, b"not an image").unwrap();
        assert!(dimensions(&bad).is_err());
    }
}
