//! Tier 2 codec: OpenEXR via the pure-Rust `exr` crate.
//!
//! OpenEXR is the film-industry HDR format — 16-bit or 32-bit float samples,
//! arbitrary channels (R, G, B, A, plus arbitrary others like diffuse,
//! specular, Z-depth), multi-part, multi-resolution (mipmaps), and deep data.
//!
//! This codec handles the common case: a single-layer image with R, G, B
//! channels and an optional A channel. Multi-channel images (e.g. with Z or
//! other arbitrary channels) decode fine — `rgba_channels` skips channels
//! it doesn't care about.
//!
//! Tone mapping: EXR stores linear HDR float. We apply a simple Reinhard
//! operator (`out = value / (1 + value)`) to compress the dynamic range to
//! 8-bit. This is a *display* tone map, not a creative grade — viewers
//! wanting accurate HDR playback should disable this in a future "HDR mode"
//! setting.

use std::io::Cursor;

use exr::prelude::{read as exr_read, ReadChannels, ReadLayers, RgbaChannels};

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

/// OpenEXR codec. Tone-maps HDR float to RGBA8 via Reinhard.
pub struct ExrCodec;

impl Codec for ExrCodec {
    fn name(&self) -> &'static str {
        "OpenEXR"
    }

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

    fn decode(&self, bytes: &[u8]) -> Result<DecodedImage, DecodeError> {
        // The `exr` crate's builder pattern: configure the reader, then
        // `from_buffered(Cursor::new(bytes))` actually runs the decode.
        //
        // `rgba_channels(create, set_pixel)` extracts the R, G, B channels
        // (required, by name) and A (optional, defaults to 1.0). Channels
        // not in this list are silently skipped — multi-channel EXRs work,
        // we just don't surface the extra channels.
        let image = exr_read()
            .no_deep_data()
            .largest_resolution_level()
            .rgba_channels(
                |resolution, _channels: &RgbaChannels| {
                    // Allocate the pixel buffer up-front.
                    Vec::with_capacity(resolution.x() * resolution.y())
                },
                |pixels: &mut Vec<(f32, f32, f32, f32)>, _pos, (r, g, b, a): (f32, f32, f32, f32)| {
                    pixels.push((r, g, b, a));
                },
            )
            .first_valid_layer()
            .all_attributes()
            .from_buffered(Cursor::new(bytes))
            .map_err(|e| DecodeError::Decode(e.to_string()))?;

        // The image is `Image<Layer<SpecificChannels<Vec<(f32,f32,f32,f32)>, RgbaChannels>>>`.
        // Navigate: image.layer_data.{size, channel_data.pixels}.
        let layer = image.layer_data;
        let width = layer.size.x() as u32;
        let height = layer.size.y() as u32;
        let float_pixels = layer.channel_data.pixels;

        // Tone-map to RGBA8 with Reinhard: out = clamp(v/(1+v), 0, 1) * 255.
        let pixels = tone_map_reinhard(&float_pixels, width, height);

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

/// Reinhard tone-mapping: `out = clamp(v / (1+v), 0, 1) * 255`.
///
/// Operates per-channel. Alpha is scaled linearly (clamped, not Reinhard —
/// HDR alpha makes no physical sense as "brightness").
fn tone_map_reinhard(floats: &[(f32, f32, f32, f32)], width: u32, height: u32) -> Vec<u8> {
    let pixel_count = (width as usize) * (height as usize);
    let mut out = Vec::with_capacity(pixel_count * 4);

    // Tiny inline closure to keep the loop body readable.
    let tone = |v: f32| -> u8 {
        // Guard against NaN / negative values from corrupt or scientific EXRs.
        let v = if v.is_finite() && v >= 0.0 { v } else { 0.0 };
        let mapped = v / (1.0 + v);
        let clamped = mapped.clamp(0.0, 1.0);
        (clamped * 255.0).round() as u8
    };

    let alpha = |a: f32| -> u8 {
        let a = if a.is_finite() && a >= 0.0 { a } else { 0.0 };
        (a.clamp(0.0, 1.0) * 255.0).round() as u8
    };

    for &(r, g, b, a) in floats {
        out.extend_from_slice(&[tone(r), tone(g), tone(b), alpha(a)]);
    }

    out
}

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

    #[test]
    fn rejects_garbage_bytes() {
        // Not a valid EXR — no magic number / header.
        let codec = ExrCodec;
        let result = codec.decode(&[0xff, 0xfe, 0xfd]);
        assert!(matches!(result, Err(DecodeError::Decode(_))));
    }
}
