//! ImageRender — renders images in PDF via Krilla's Image API.

use std::sync::Arc;

/// Image format detected from data.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ImageFormat {
    Png,
    Jpeg,
    Gif,
}

impl ImageFormat {
    /// Create a krilla Image from raw data in this format.
    #[allow(clippy::result_unit_err)]
    pub fn to_krilla_image(self, data: krilla::Data) -> Result<krilla::image::Image, ()> {
        match self {
            ImageFormat::Png => krilla::image::Image::from_png(data, true).map_err(|_| ()),
            ImageFormat::Jpeg => krilla::image::Image::from_jpeg(data, true).map_err(|_| ()),
            ImageFormat::Gif => krilla::image::Image::from_gif(data, true).map_err(|_| ()),
        }
    }
}

/// Classification of an asset's bytes for rendering path selection.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssetKind {
    /// Raster image (PNG/JPEG/GIF) → renders via `ImageRender`.
    Raster(ImageFormat),
    /// SVG vector image → renders via `SvgRender`.
    Svg,
    /// Unrecognized / unsupported.
    Unknown,
}

impl AssetKind {
    /// Classify raw asset bytes by sniffing the header.
    ///
    /// Accepts SVG via the `<svg` element start or `<?xml` prolog, tolerant
    /// of an optional leading UTF-8 BOM and ASCII whitespace. Falls through
    /// to `ImageRender::detect_format` for raster magic bytes.
    pub fn detect(data: &[u8]) -> AssetKind {
        if let Some(format) = ImageRender::detect_format(data) {
            return AssetKind::Raster(format);
        }
        let mut slice = data;
        // Strip optional UTF-8 BOM so `<svg` / `<?xml` detection works.
        if slice.starts_with(b"\xEF\xBB\xBF") {
            slice = &slice[3..];
        }
        // Skip ASCII whitespace that commonly precedes the SVG root.
        while let Some((first, rest)) = slice.split_first() {
            if first.is_ascii_whitespace() {
                slice = rest;
            } else {
                break;
            }
        }
        if slice.starts_with(b"<?xml") || slice.starts_with(b"<svg") {
            return AssetKind::Svg;
        }
        AssetKind::Unknown
    }
}

/// An image element that renders an image at its computed size.
#[derive(Clone)]
pub struct ImageRender {
    pub image_data: Arc<Vec<u8>>,
    pub format: ImageFormat,
    pub width: f32,
    pub height: f32,
    pub opacity: f32,
    pub visible: bool,
    /// fulgur-r6we (Phase 3.2.a): DOM NodeId for `slice_for_page`
    /// geometry lookup. See `BlockPageable::node_id`.
    pub node_id: Option<usize>,
}

impl ImageRender {
    pub fn new(data: Arc<Vec<u8>>, format: ImageFormat, width: f32, height: f32) -> Self {
        Self {
            image_data: data,
            format,
            width,
            height,
            opacity: 1.0,
            visible: true,
            node_id: None,
        }
    }

    pub fn with_node_id(mut self, node_id: Option<usize>) -> Self {
        self.node_id = node_id;
        self
    }

    /// Decode image dimensions (width, height) from header bytes.
    /// Returns None if the data is too short or malformed.
    pub fn decode_dimensions(data: &[u8], format: ImageFormat) -> Option<(u32, u32)> {
        match format {
            ImageFormat::Png => {
                // PNG IHDR: bytes 16..20 = width (BE u32), 20..24 = height (BE u32)
                if data.len() < 24 {
                    return None;
                }
                let w = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
                let h = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
                Some((w, h))
            }
            ImageFormat::Gif => {
                // GIF header: bytes 6..8 = width (LE u16), 8..10 = height (LE u16)
                if data.len() < 10 {
                    return None;
                }
                let w = u16::from_le_bytes([data[6], data[7]]) as u32;
                let h = u16::from_le_bytes([data[8], data[9]]) as u32;
                Some((w, h))
            }
            ImageFormat::Jpeg => {
                // JPEG: scan for SOF0..SOF15 markers (0xC0..0xCF, excluding 0xC4 DHT and 0xCC DAC)
                // SOF structure: FF Cx, 2-byte length, 1 byte precision, 2 bytes height (BE), 2 bytes width (BE)
                if data.len() < 2 {
                    return None;
                }
                let mut i = 2; // skip SOI (FF D8)
                while i + 1 < data.len() {
                    if data[i] != 0xFF {
                        i += 1;
                        continue;
                    }
                    let marker = data[i + 1];
                    i += 2;
                    if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xCC {
                        if i + 7 > data.len() {
                            return None;
                        }
                        let h = u16::from_be_bytes([data[i + 3], data[i + 4]]) as u32;
                        let w = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32;
                        return Some((w, h));
                    }
                    if i + 1 >= data.len() {
                        return None;
                    }
                    let seg_len = u16::from_be_bytes([data[i], data[i + 1]]) as usize;
                    if seg_len < 2 {
                        return None;
                    }
                    i += seg_len;
                }
                None
            }
        }
    }

    /// Detect image format from data magic bytes.
    pub fn detect_format(data: &[u8]) -> Option<ImageFormat> {
        if data.starts_with(b"\x89PNG") {
            Some(ImageFormat::Png)
        } else if data.starts_with(b"\xFF\xD8\xFF") {
            Some(ImageFormat::Jpeg)
        } else if data.starts_with(b"GIF") {
            Some(ImageFormat::Gif)
        } else {
            None
        }
    }
}

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

    // 1x1 red PNG
    const MINIMAL_PNG: &[u8] = &[
        0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44,
        0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90,
        0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0xF8,
        0xCF, 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, 0xC9, 0xFE, 0x92, 0xEF, 0x00, 0x00, 0x00,
        0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
    ];

    // 1x1 white GIF89a
    const MINIMAL_GIF: &[u8] = &[
        0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x3B,
    ];

    #[test]
    fn test_png_dimensions() {
        let dims = ImageRender::decode_dimensions(MINIMAL_PNG, ImageFormat::Png);
        assert_eq!(dims, Some((1, 1)));
    }

    #[test]
    fn test_gif_dimensions() {
        let dims = ImageRender::decode_dimensions(MINIMAL_GIF, ImageFormat::Gif);
        assert_eq!(dims, Some((1, 1)));
    }

    // Minimal 1x1 white JPEG (SOF0 baseline)
    const MINIMAL_JPEG: &[u8] = &[
        0xFF, 0xD8, // SOI
        0xFF, 0xE0, // APP0 (JFIF)
        0x00, 0x10, // length = 16
        0x4A, 0x46, 0x49, 0x46, 0x00, // "JFIF\0"
        0x01, 0x01, // version
        0x00, // units
        0x00, 0x01, // X density
        0x00, 0x01, // Y density
        0x00, 0x00, // thumbnail
        0xFF, 0xC0, // SOF0
        0x00, 0x0B, // length = 11
        0x08, // precision = 8
        0x00, 0x01, // height = 1
        0x00, 0x01, // width = 1
        0x01, // components = 1
        0x01, 0x11, 0x00, // component 1
        0xFF, 0xD9, // EOI
    ];

    #[test]
    fn test_jpeg_dimensions() {
        let dims = ImageRender::decode_dimensions(MINIMAL_JPEG, ImageFormat::Jpeg);
        assert_eq!(dims, Some((1, 1)));
    }

    #[test]
    fn test_truncated_data_returns_none() {
        let dims = ImageRender::decode_dimensions(&[0x89, 0x50], ImageFormat::Png);
        assert_eq!(dims, None);
    }

    #[test]
    fn test_detect_asset_kind_png() {
        assert!(matches!(
            AssetKind::detect(MINIMAL_PNG),
            AssetKind::Raster(ImageFormat::Png)
        ));
    }

    #[test]
    fn test_detect_asset_kind_jpeg() {
        assert!(matches!(
            AssetKind::detect(MINIMAL_JPEG),
            AssetKind::Raster(ImageFormat::Jpeg)
        ));
    }

    #[test]
    fn test_detect_asset_kind_gif() {
        assert!(matches!(
            AssetKind::detect(MINIMAL_GIF),
            AssetKind::Raster(ImageFormat::Gif)
        ));
    }

    #[test]
    fn test_detect_asset_kind_svg_tag() {
        let svg = br#"<svg xmlns="http://www.w3.org/2000/svg"></svg>"#;
        assert!(matches!(AssetKind::detect(svg), AssetKind::Svg));
    }

    #[test]
    fn test_detect_asset_kind_svg_xml_prolog() {
        let svg = br#"<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg"></svg>"#;
        assert!(matches!(AssetKind::detect(svg), AssetKind::Svg));
    }

    #[test]
    fn test_detect_asset_kind_svg_with_utf8_bom() {
        let svg = b"\xEF\xBB\xBF<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>";
        assert!(matches!(AssetKind::detect(svg), AssetKind::Svg));
    }

    #[test]
    fn test_detect_asset_kind_empty() {
        assert!(matches!(AssetKind::detect(&[]), AssetKind::Unknown));
    }

    #[test]
    fn test_detect_asset_kind_unknown() {
        assert!(matches!(
            AssetKind::detect(b"not an image"),
            AssetKind::Unknown
        ));
    }

    #[test]
    fn image_render_new_initialises_defaults_and_with_node_id_overrides() {
        let data = Arc::new(MINIMAL_PNG.to_vec());
        let img = ImageRender::new(data.clone(), ImageFormat::Png, 100.0, 50.0);
        assert_eq!(img.width, 100.0);
        assert_eq!(img.height, 50.0);
        assert_eq!(img.opacity, 1.0);
        assert!(img.visible);
        assert!(img.node_id.is_none());
        let img2 = img.with_node_id(Some(42));
        assert_eq!(img2.node_id, Some(42));
        let img3 = img2.with_node_id(None);
        assert!(img3.node_id.is_none());
    }

    // --- to_krilla_image: cover the Jpeg and Gif match arms ---

    #[test]
    fn to_krilla_image_jpeg_invalid_returns_err() {
        // Exercises the Jpeg arm and the map_err closure.
        let data: krilla::Data = b"not a jpeg".to_vec().into();
        assert!(ImageFormat::Jpeg.to_krilla_image(data).is_err());
    }

    #[test]
    fn to_krilla_image_gif_invalid_returns_err() {
        // Exercises the Gif arm and the map_err closure.
        let data: krilla::Data = b"not a gif".to_vec().into();
        assert!(ImageFormat::Gif.to_krilla_image(data).is_err());
    }

    #[test]
    fn to_krilla_image_png_invalid_returns_err() {
        // Exercises the Png arm error path.
        let data: krilla::Data = b"not a png".to_vec().into();
        assert!(ImageFormat::Png.to_krilla_image(data).is_err());
    }

    // --- JPEG decode_dimensions edge cases ---

    #[test]
    fn jpeg_decode_dimensions_too_short_returns_none() {
        // Hits the `data.len() < 2` early-return.
        assert_eq!(ImageRender::decode_dimensions(&[], ImageFormat::Jpeg), None);
        assert_eq!(
            ImageRender::decode_dimensions(&[0xFF], ImageFormat::Jpeg),
            None
        );
    }

    #[test]
    fn jpeg_decode_dimensions_non_0xff_byte_skipped() {
        // A garbage non-0xFF byte at index 2 is skipped (i += 1), then a valid
        // SOF0 marker is found and dimensions are read correctly.
        let data: &[u8] = &[
            0xFF, 0xD8, // SOI
            0xAA, // non-0xFF garbage — triggers `i += 1; continue`
            0xFF, 0xC0, // SOF0
            0x00, 0x0B, // segment length = 11
            0x08, // sample precision
            0x00, 0x02, // height = 2
            0x00, 0x03, // width = 3
            0x01, // components
        ];
        assert_eq!(
            ImageRender::decode_dimensions(data, ImageFormat::Jpeg),
            Some((3, 2))
        );
    }

    #[test]
    fn jpeg_decode_dimensions_truncated_at_sof_returns_none() {
        // SOF0 marker is found but fewer than 7 payload bytes remain.
        let data: &[u8] = &[
            0xFF, 0xD8, // SOI (i = 2)
            0xFF, 0xC0, // SOF0 (i → 4)
            0x00, // only 1 byte left; need i+7=11, have 5 → None
        ];
        assert_eq!(
            ImageRender::decode_dimensions(data, ImageFormat::Jpeg),
            None
        );
    }

    #[test]
    fn jpeg_decode_dimensions_truncated_before_seg_len_returns_none() {
        // Non-SOF marker at end of data; no bytes remain for the segment length.
        // Hits the `i + 1 >= data.len()` early-return.
        let data: &[u8] = &[
            0xFF, 0xD8, // SOI (i = 2)
            0xFF, 0xE0, // APP0 (i → 4); need data[4..6] for seg_len but len = 4
        ];
        assert_eq!(
            ImageRender::decode_dimensions(data, ImageFormat::Jpeg),
            None
        );
    }

    #[test]
    fn jpeg_decode_dimensions_seg_len_too_small_returns_none() {
        // Segment length field value < 2 — hits the `seg_len < 2` guard.
        let data: &[u8] = &[
            0xFF, 0xD8, // SOI (i = 2)
            0xFF, 0xE0, // APP0 (i → 4)
            0x00, 0x01, // seg_len = 1 < 2 → None
        ];
        assert_eq!(
            ImageRender::decode_dimensions(data, ImageFormat::Jpeg),
            None
        );
    }

    #[test]
    fn jpeg_decode_dimensions_no_sof_marker_returns_none() {
        // A complete non-SOF segment is consumed; the while loop exits naturally
        // without having found a SOF marker, so the function returns None.
        let data: &[u8] = &[
            0xFF, 0xD8, // SOI (i = 2)
            0xFF, 0xE0, // APP0 (i → 4)
            0x00, 0x04, // seg_len = 4 (i → 4 + 4 = 8)
            0x00,
            0x00, // APP0 payload (2 bytes)
                  // i = 8; 8+1 = 9 >= data.len()=8 → loop exits → None
        ];
        assert_eq!(
            ImageRender::decode_dimensions(data, ImageFormat::Jpeg),
            None
        );
    }

    // --- GIF edge case ---

    #[test]
    fn gif_decode_dimensions_too_short_returns_none() {
        // data.len() < 10 hits the early-return guard.
        assert_eq!(
            ImageRender::decode_dimensions(b"GIF89a\x01\x00\x01", ImageFormat::Gif),
            None
        );
    }

    // --- AssetKind::detect whitespace-skipping path ---

    #[test]
    fn detect_svg_with_leading_whitespace() {
        // ASCII whitespace before `<svg` exercises the `slice = rest` branch in
        // the whitespace-skipping loop inside AssetKind::detect.
        let svg = b"  \n\t<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>";
        assert!(matches!(AssetKind::detect(svg), AssetKind::Svg));
    }
}
