//! Tier 2 codec: TIFF via the pure-Rust `tiff` crate.
//!
//! TIFF is the open archival master format — 8/16/32-bit integer or float
//! samples, RGB/RGBA/Gray/GrayA/CMYK/YCbCr/Lab color types, multi-page
//! documents, ICC profiles, lossless compression (LZW, Deflate, PackBits).
//! The `image` crate's TIFF support is good but lags behind `tiff` directly
//! for unusual photometric interpretations; this codec gives us first-class
//! access.
//!
//! Notes / limitations:
//! - Multi-page TIFFs decode page 0 only. A future "page navigator" feature
//!   would iterate `decoder.next_image()` and surface each page; for now, the
//!   cover page is the right default for a still-image viewer.
//! - 16-bit samples are downscaled to 8-bit by taking the high byte. This is
//!   a lossy operation — the low byte (which differentiates ~65k levels down
//!   to ~256) is discarded. We do not gamma-correct or apply a tone curve
//!   here; that belongs in a future "HDR display" feature.

use std::io::Cursor;

use tiff::decoder::{Decoder, DecodingResult};
use tiff::ColorType;

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

/// TIFF codec. Decodes page 0 of multi-page TIFFs.
pub struct TiffCodec;

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

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

    fn decode(&self, bytes: &[u8]) -> Result<DecodedImage, DecodeError> {
        // Page 0 only: a freshly-constructed `Decoder` points at the first
        // IFD. Callers wanting later pages would call `decoder.next_image()`.
        let mut decoder = Decoder::new(Cursor::new(bytes))
            .map_err(|e| DecodeError::Decode(e.to_string()))?;

        let (width, height) = decoder
            .dimensions()
            .map_err(|e| DecodeError::Decode(e.to_string()))?;

        let color_type = decoder
            .colortype()
            .map_err(|e| DecodeError::Decode(e.to_string()))?;

        let result = decoder
            .read_image()
            .map_err(|e| DecodeError::Decode(e.to_string()))?;

        let pixels = convert_to_rgba(result, color_type, width, height);

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

/// Convert a `DecodingResult` + `ColorType` pair into RGBA8.
///
/// The `tiff` crate hands us the raw sample buffer + a tag describing how to
/// interpret it. We expand every supported layout into RGBA8 here.
fn convert_to_rgba(
    result: DecodingResult,
    color_type: ColorType,
    width: u32,
    height: u32,
) -> Vec<u8> {
    let pixel_count = (width as usize) * (height as usize);
    let mut out = Vec::with_capacity(pixel_count * 4);

    // 16-bit downscale helper — keep the high byte only. See module docs for
    // why we don't tone-map.
    fn take_u8_high(samples: &[u16], out: &mut Vec<u8>, expand: impl Fn(u8, &mut Vec<u8>)) {
        for &s in samples {
            expand((s >> 8) as u8, out);
        }
    }

    match (color_type, result) {
        // ── 8-bit types ────────────────────────────────────────────────
        (ColorType::Gray(_), DecodingResult::U8(buf)) => {
            for &g in &buf {
                out.extend_from_slice(&[g, g, g, 255]);
            }
        }
        (ColorType::GrayA(_), DecodingResult::U8(buf)) => {
            for chunk in buf.chunks_exact(2) {
                out.extend_from_slice(&[chunk[0], chunk[0], chunk[0], chunk[1]]);
            }
        }
        (ColorType::RGB(_), DecodingResult::U8(buf)) => {
            for chunk in buf.chunks_exact(3) {
                out.extend_from_slice(&[chunk[0], chunk[1], chunk[2], 255]);
            }
        }
        (ColorType::RGBA(_), DecodingResult::U8(buf)) => {
            out.extend_from_slice(&buf);
        }
        (ColorType::CMYK(_), DecodingResult::U8(buf)) => {
            // Naive CMYK→RGB: r = 255 - min(c+m, 255) etc.
            // (Real CMYK conversion needs an ICC profile; this is a fallback.)
            for chunk in buf.chunks_exact(4) {
                let c = chunk[0] as u32;
                let m = chunk[1] as u32;
                let y = chunk[2] as u32;
                let k = chunk[3] as u32;
                let r = 255u32.saturating_sub(c + k);
                let g = 255u32.saturating_sub(m + k);
                let b = 255u32.saturating_sub(y + k);
                out.extend_from_slice(&[r as u8, g as u8, b as u8, 255]);
            }
        }
        (ColorType::Palette(_), DecodingResult::U8(_buf)) => {
            // Palette TIFF requires the color-map tag, which we don't fetch
            // here. Fill transparent-black so the user sees a clear failure.
            out.resize(pixel_count * 4, 0);
            for px in out.chunks_exact_mut(4) {
                px[3] = 255;
            }
        }

        // ── 16-bit types (downscaled to 8-bit; lossy) ─────────────────
        (ColorType::Gray(_), DecodingResult::U16(buf)) => {
            take_u8_high(&buf, &mut out, |g, o| o.extend_from_slice(&[g, g, g, 255]));
        }
        (ColorType::GrayA(_), DecodingResult::U16(buf)) => {
            for chunk in buf.chunks_exact(2) {
                let g = (chunk[0] >> 8) as u8;
                let a = (chunk[1] >> 8) as u8;
                out.extend_from_slice(&[g, g, g, a]);
            }
        }
        (ColorType::RGB(_), DecodingResult::U16(buf)) => {
            for chunk in buf.chunks_exact(3) {
                out.extend_from_slice(&[
                    (chunk[0] >> 8) as u8,
                    (chunk[1] >> 8) as u8,
                    (chunk[2] >> 8) as u8,
                    255,
                ]);
            }
        }
        (ColorType::RGBA(_), DecodingResult::U16(buf)) => {
            for chunk in buf.chunks_exact(4) {
                out.extend_from_slice(&[
                    (chunk[0] >> 8) as u8,
                    (chunk[1] >> 8) as u8,
                    (chunk[2] >> 8) as u8,
                    (chunk[3] >> 8) as u8,
                ]);
            }
        }

        // ── Everything else: opaque black fallback ────────────────────
        // Covers YCbCr, Lab, Multiband, float samples, and any unexpected
        // combination. The user sees a clearly broken image rather than a
        // panic; the codec returns `Ok` because the bytes did decode.
        _ => {
            out.resize(pixel_count * 4, 0);
            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 — not a valid TIFF (no `II*\0` or `MM\0*` magic).
        let codec = TiffCodec;
        let result = codec.decode(&[0xff, 0xfe, 0xfd]);
        assert!(matches!(result, Err(DecodeError::Decode(_))));
    }
}
