//! Tier 2 codec: JPEG XL via the pure-Rust `jxl-oxide` crate.
//!
//! JPEG XL is the modern royalty-free successor to JPEG — better compression
//! at the same perceptual quality, lossless transcode from legacy JPEG, support
//! for 16-bit, alpha, animation, and HDR. We decode only the first keyframe
//! here; animation support is a future concern (see DECISION.md D004).
//!
//! Performance note: jxl-oxide is pure Rust with no SIMD intrinsics on most
//! targets. Decoding a large JXL master (50+ MP) can take seconds where a
//! libjxl-backed decoder would take milliseconds. This is acceptable for an
//! image viewer's "open on click" use case; if it ever becomes a hotspot,
//! swap in `zune-jpegxl` or a libjxl FFI.

use std::io::Cursor;

use jxl_oxide::{JxlImage, PixelFormat};

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

/// JPEG XL codec. Stateless — each `decode` call constructs its own `JxlImage`.
pub struct JxlCodec;

impl Codec for JxlCodec {
    fn name(&self) -> &'static str {
        "JPEG XL"
    }

    fn extensions(&self) -> &'static [&'static str] {
        &["jxl"]
    }

    fn decode(&self, bytes: &[u8]) -> Result<DecodedImage, DecodeError> {
        // `JxlImage::from_memory` was the API in jxl-oxide 0.8; in 0.12 the
        // entry point is `JxlImage::builder().read(reader)` (or
        // `read_with_defaults`). We pass a `Cursor` so the entire byte slice
        // is treated as the file contents.
        let img = JxlImage::builder()
            .read(&mut Cursor::new(bytes))
            .map_err(|e| DecodeError::Decode(e.to_string()))?;

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

        // Render only the first keyframe. JXL supports animation; this codec
        // surfaces frame 0 as the still image (matching the `image` crate's
        // behavior for animated GIF/WebP).
        let render = img
            .render_frame(0)
            .map_err(|e| DecodeError::Decode(e.to_string()))?;

        let mut stream = render.stream();
        let channels = stream.channels() as usize;

        // Allocate a u8 buffer sized to the native channel layout. We expand
        // to RGBA8 below.
        let native_len = (width as usize) * (height as usize) * channels;
        let mut raw = vec![0u8; native_len];
        stream.write_to_buffer(&mut raw);

        let pixels = normalize_to_rgba(&raw, img.pixel_format(), width, height);

        Ok(DecodedImage {
            width,
            height,
            pixels,
            format: "JPEG XL",
        })
    }
}

/// Expand an arbitrary pixel-format buffer to RGBA8.
///
/// JXL pixel layouts we care about:
/// - `Gray`   (1ch) → R=G=B=gray, A=255
/// - `Graya`  (2ch) → R=G=B=gray, A=alpha
/// - `Rgb`    (3ch) → as-is, A=255
/// - `Rgba`   (4ch) → as-is
///
/// `Cmyk` / `Cmyka` are not handled here — the caller is expected to have
/// requested an sRGB color encoding for those, which downgrades them to
/// RGB(A). If the request fails or is bypassed, the buffer will be wrong;
/// that's an acceptable failure mode for an image viewer (we surface a
/// decode error rather than render wrong colors silently).
fn normalize_to_rgba(
    raw: &[u8],
    format: PixelFormat,
    width: u32,
    height: u32,
) -> Vec<u8> {
    let pixel_count = (width as usize) * (height as usize);
    let mut out = Vec::with_capacity(pixel_count * 4);

    match format {
        PixelFormat::Gray => {
            for &g in raw.iter() {
                out.extend_from_slice(&[g, g, g, 255]);
            }
        }
        PixelFormat::Graya => {
            for chunk in raw.chunks_exact(2) {
                let g = chunk[0];
                let a = chunk[1];
                out.extend_from_slice(&[g, g, g, a]);
            }
        }
        PixelFormat::Rgb => {
            for chunk in raw.chunks_exact(3) {
                out.extend_from_slice(&[chunk[0], chunk[1], chunk[2], 255]);
            }
        }
        PixelFormat::Rgba => {
            out.extend_from_slice(raw);
        }
        // CMYK should be converted upstream via `request_color_encoding(sRGB)`.
        // If we get here, the conversion didn't apply — render as opaque black
        // so the user sees a clearly broken image rather than a crash.
        PixelFormat::Cmyk | PixelFormat::Cmyka => {
            out.resize(pixel_count * 4, 0);
            // Set alpha=255 so the (black) pixels are at least visible.
            for px in out.chunks_exact_mut(4) {
                px[3] = 255;
            }
        }
    }

    out
}

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

    #[test]
    fn rejects_garbage_bytes() {
        // Three bytes that aren't a valid JXL codestream or container —
        // jxl-oxide should reject them at header-parse time.
        let codec = JxlCodec;
        let result = codec.decode(&[0xff, 0xfe, 0xfd]);
        assert!(matches!(result, Err(DecodeError::Decode(_))));
    }
}
