//! ICC profile parsing and color conversion
//!
//! Implements ICC profile binary format parsing, tone curve extraction,
//! and color conversion between profile colorspace and PCS (XYZ).

use crate::fitz::error::{Error, Result};
use byteorder::{BigEndian, ReadBytesExt};
use std::collections::HashMap;
use std::io::{Cursor, Read};
use std::sync::RwLock;

// ============================================================================
// Enums
// ============================================================================

/// ICC Profile Connection Space (PCS) color space
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IccColorSpace {
    XYZ,
    Lab,
    Luv,
    YCbCr,
    Yxy,
    RGB,
    Gray,
    HSV,
    HLS,
    CMYK,
    CMY,
    /// n-channel color (n = 2..15)
    NClr(u16),
}

impl IccColorSpace {
    fn from_sig(sig: u32) -> Option<Self> {
        match sig {
            0x58595A20 => Some(Self::XYZ),   // 'XYZ '
            0x4C616220 => Some(Self::Lab),   // 'Lab '
            0x4C757620 => Some(Self::Luv),   // 'Luv '
            0x59436272 => Some(Self::YCbCr), // 'Ycbr'
            0x59787920 => Some(Self::Yxy),   // 'Yxy '
            0x52474220 => Some(Self::RGB),   // 'RGB '
            0x47524159 => Some(Self::Gray),  // 'GRAY'
            0x48535620 => Some(Self::HSV),   // 'HSV '
            0x484C5320 => Some(Self::HLS),   // 'HLS '
            0x434D594B => Some(Self::CMYK),  // 'CMYK'
            0x434D5920 => Some(Self::CMY),   // 'CMY '
            0x32434C52 => Some(Self::NClr(2)),
            0x33434C52 => Some(Self::NClr(3)),
            0x34434C52 => Some(Self::NClr(4)),
            0x35434C52 => Some(Self::NClr(5)),
            0x36434C52 => Some(Self::NClr(6)),
            0x37434C52 => Some(Self::NClr(7)),
            0x38434C52 => Some(Self::NClr(8)),
            0x39434C52 => Some(Self::NClr(9)),
            0x41434C52 => Some(Self::NClr(10)),
            0x42434C52 => Some(Self::NClr(11)),
            0x43434C52 => Some(Self::NClr(12)),
            0x44434C52 => Some(Self::NClr(13)),
            0x45434C52 => Some(Self::NClr(14)),
            0x46434C52 => Some(Self::NClr(15)),
            _ => None,
        }
    }
}

/// ICC device class
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IccDeviceClass {
    Input,
    Display,
    Output,
    DeviceLink,
    ColorSpace,
    Abstract,
    NamedColor,
}

impl IccDeviceClass {
    fn from_sig(sig: u32) -> Option<Self> {
        match sig {
            0x73636E72 => Some(Self::Input),      // 'scnr'
            0x6D6E7472 => Some(Self::Display),    // 'mntr'
            0x70727472 => Some(Self::Output),     // 'prtr'
            0x6C696E6B => Some(Self::DeviceLink), // 'link'
            0x73706163 => Some(Self::ColorSpace), // 'spac'
            0x61627374 => Some(Self::Abstract),   // 'abst'
            0x6E6D636C => Some(Self::NamedColor), // 'nmcl'
            _ => None,
        }
    }
}

/// ICC rendering intent
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderingIntent {
    Perceptual,
    RelativeColorimetric,
    Saturation,
    AbsoluteColorimetric,
}

impl RenderingIntent {
    fn from_u32(v: u32) -> Self {
        match v {
            0 => Self::Perceptual,
            1 => Self::RelativeColorimetric,
            2 => Self::Saturation,
            3 => Self::AbsoluteColorimetric,
            _ => Self::Perceptual,
        }
    }
}

/// Tone response curve representation
#[derive(Debug, Clone, PartialEq)]
pub enum IccToneCurve {
    /// Single gamma value (curv with count=1)
    Gamma(f64),
    /// Parametric curve (para tag) - function type + parameters
    Parametric(Vec<f64>),
    /// Lookup table (curv with count>1)
    Table(Vec<u16>),
}

// ============================================================================
// Header
// ============================================================================

/// ICC profile header (128 bytes)
#[derive(Debug, Clone)]
pub struct IccHeader {
    pub size: u32,
    pub preferred_cmm: u32,
    pub version_major: u8,
    pub version_minor: u8,
    pub device_class: IccDeviceClass,
    pub color_space: IccColorSpace,
    pub pcs: [u8; 4],
    pub creation_date: [u8; 12],
    pub primary_platform: u32,
    pub rendering_intent: RenderingIntent,
}

// ============================================================================
// Tag table
// ============================================================================

/// Tag table entry
#[derive(Debug, Clone)]
pub struct IccTagEntry {
    pub sig: u32,
    pub offset: u32,
    pub size: u32,
}

// Tag signatures
const TAG_RXYZ: u32 = 0x7258595A; // 'rXYZ'
const TAG_GXYZ: u32 = 0x6758595A; // 'gXYZ'
const TAG_BXYZ: u32 = 0x6258595A; // 'bXYZ'
const TAG_RTRC: u32 = 0x72545243; // 'rTRC'
const TAG_GTRC: u32 = 0x67545243; // 'gTRC'
const TAG_BTRC: u32 = 0x62545243; // 'bTRC'
const TAG_WTPT: u32 = 0x77747074; // 'wtpt'
const TAG_DESC: u32 = 0x64657363; // 'desc'
const TAG_KTRC: u32 = 0x6B545243; // 'kTRC' (gray)

// Tag type signatures
const TYPE_XYZ: u32 = 0x58595A20; // 'XYZ '
const TYPE_CURV: u32 = 0x63757276; // 'curv'
const TYPE_PARA: u32 = 0x70617261; // 'para'

// ============================================================================
// IccProfile
// ============================================================================

/// Parsed ICC profile with header, tag table, and raw data
#[derive(Debug, Clone)]
pub struct IccProfile {
    pub header: IccHeader,
    pub tag_table: Vec<IccTagEntry>,
    pub raw_data: Vec<u8>,
}

impl IccProfile {
    /// Get tag data by signature
    pub fn get_tag(&self, sig: u32) -> Option<&[u8]> {
        let entry = self.tag_table.iter().find(|e| e.sig == sig)?;
        let start = entry.offset as usize;
        let end = start.saturating_add(entry.size as usize);
        if end <= self.raw_data.len() {
            Some(&self.raw_data[start..end])
        } else {
            None
        }
    }

    /// Parse XYZ from xyz_type tag (type 4 + reserved 4 + X 4 + Y 4 + Z 4 = 20 bytes)
    fn parse_xyz_tag(&self, sig: u32) -> Option<[f64; 3]> {
        let data = self.get_tag(sig)?;
        if data.len() < 20 {
            return None;
        }
        let x = read_s15_fixed16(&data[8..12]);
        let y = read_s15_fixed16(&data[12..16]);
        let z = read_s15_fixed16(&data[16..20]);
        Some([x, y, z])
    }

    /// Get red, green, blue matrix columns (XYZ) for RGB profile
    pub fn rgb_primaries(&self) -> Option<[[f64; 3]; 3]> {
        let r = self.parse_xyz_tag(TAG_RXYZ)?;
        let g = self.parse_xyz_tag(TAG_GXYZ)?;
        let b = self.parse_xyz_tag(TAG_BXYZ)?;
        Some([r, g, b])
    }

    /// Get white point
    pub fn white_point(&self) -> Option<[f64; 3]> {
        self.parse_xyz_tag(TAG_WTPT)
    }

    /// Get tone curve for a channel (rTRC, gTRC, bTRC, or kTRC)
    pub fn tone_curve(&self, sig: u32) -> Option<IccToneCurve> {
        let data = self.get_tag(sig)?;
        parse_tone_curve(data)
    }

    /// Get RGB tone curves
    pub fn rgb_tone_curves(&self) -> Option<[IccToneCurve; 3]> {
        let r = self.tone_curve(TAG_RTRC)?;
        let g = self.tone_curve(TAG_GTRC)?;
        let b = self.tone_curve(TAG_BTRC)?;
        Some([r, g, b])
    }
}

// ============================================================================
// Parsing
// ============================================================================

fn read_s15_fixed16(data: &[u8]) -> f64 {
    if data.len() < 4 {
        return 0.0;
    }
    let raw = i32::from_be_bytes([data[0], data[1], data[2], data[3]]);
    raw as f64 / 65536.0
}

fn read_u8_fixed8(data: &[u8]) -> f64 {
    if data.len() < 2 {
        return 1.0;
    }
    let raw = u16::from_be_bytes([data[0], data[1]]);
    raw as f64 / 256.0
}

/// Parse ICC profile from raw binary data
pub fn parse_icc_profile(data: &[u8]) -> Result<IccProfile> {
    if data.len() < 128 {
        return Err(Error::format("ICC profile too short for header"));
    }

    let mut r = Cursor::new(data);

    let size = r
        .read_u32::<BigEndian>()
        .map_err(|e| Error::format(e.to_string()))?;
    let preferred_cmm = r
        .read_u32::<BigEndian>()
        .map_err(|e| Error::format(e.to_string()))?;

    let version = r
        .read_u32::<BigEndian>()
        .map_err(|e| Error::format(e.to_string()))?;
    let version_major = (version >> 24) as u8;
    let version_minor = ((version >> 16) & 0xFF) as u8;

    let device_class_sig = r
        .read_u32::<BigEndian>()
        .map_err(|e| Error::format(e.to_string()))?;
    let device_class =
        IccDeviceClass::from_sig(device_class_sig).unwrap_or(IccDeviceClass::Display);

    let color_space_sig = r
        .read_u32::<BigEndian>()
        .map_err(|e| Error::format(e.to_string()))?;
    let color_space = IccColorSpace::from_sig(color_space_sig).unwrap_or(IccColorSpace::RGB);

    let mut pcs = [0u8; 4];
    r.read_exact(&mut pcs)
        .map_err(|e| Error::format(e.to_string()))?;

    let mut creation_date = [0u8; 12];
    r.read_exact(&mut creation_date)
        .map_err(|e| Error::format(e.to_string()))?;

    let mut file_sig = [0u8; 4];
    r.read_exact(&mut file_sig)
        .map_err(|e| Error::format(e.to_string()))?;
    if &file_sig != b"acsp" {
        return Err(Error::format("ICC profile missing 'acsp' signature"));
    }

    let primary_platform = r
        .read_u32::<BigEndian>()
        .map_err(|e| Error::format(e.to_string()))?;
    let _profile_flags = r
        .read_u32::<BigEndian>()
        .map_err(|e| Error::format(e.to_string()))?;
    let _device_manufacturer = r
        .read_u32::<BigEndian>()
        .map_err(|e| Error::format(e.to_string()))?;
    let mut _device_model = [0u8; 4];
    r.read_exact(&mut _device_model)
        .map_err(|e| Error::format(e.to_string()))?;
    let mut _device_attributes = [0u8; 8];
    r.read_exact(&mut _device_attributes)
        .map_err(|e| Error::format(e.to_string()))?;

    let rendering_intent_u32 = r
        .read_u32::<BigEndian>()
        .map_err(|e| Error::format(e.to_string()))?;
    let rendering_intent = RenderingIntent::from_u32(rendering_intent_u32);

    let _illuminant = r
        .read_u32::<BigEndian>()
        .map_err(|e| Error::format(e.to_string()))?;
    let _illuminant_y = r
        .read_u32::<BigEndian>()
        .map_err(|e| Error::format(e.to_string()))?;
    let _illuminant_z = r
        .read_u32::<BigEndian>()
        .map_err(|e| Error::format(e.to_string()))?;
    let mut _creator = [0u8; 4];
    r.read_exact(&mut _creator)
        .map_err(|e| Error::format(e.to_string()))?;
    let mut _identifier = [0u8; 16];
    r.read_exact(&mut _identifier)
        .map_err(|e| Error::format(e.to_string()))?;
    let mut _reserved = [0u8; 28];
    r.read_exact(&mut _reserved)
        .map_err(|e| Error::format(e.to_string()))?;

    let header = IccHeader {
        size,
        preferred_cmm,
        version_major,
        version_minor,
        device_class,
        color_space,
        pcs,
        creation_date,
        primary_platform,
        rendering_intent,
    };

    if data.len() < 132 {
        return Ok(IccProfile {
            header,
            tag_table: Vec::new(),
            raw_data: data.to_vec(),
        });
    }

    let tag_count = u32::from_be_bytes([data[128], data[129], data[130], data[131]]) as usize;
    let tag_table_size = 12 * tag_count;
    if data.len() < 132 + tag_table_size {
        return Err(Error::format("ICC profile truncated at tag table"));
    }

    let mut tag_table = Vec::with_capacity(tag_count);
    for i in 0..tag_count {
        let base = 132 + i * 12;
        let sig = u32::from_be_bytes([data[base], data[base + 1], data[base + 2], data[base + 3]]);
        let offset = u32::from_be_bytes([
            data[base + 4],
            data[base + 5],
            data[base + 6],
            data[base + 7],
        ]);
        let size = u32::from_be_bytes([
            data[base + 8],
            data[base + 9],
            data[base + 10],
            data[base + 11],
        ]);
        tag_table.push(IccTagEntry { sig, offset, size });
    }

    Ok(IccProfile {
        header,
        tag_table,
        raw_data: data.to_vec(),
    })
}

/// Parse tone curve from curv or para tag data
pub fn parse_tone_curve(data: &[u8]) -> Option<IccToneCurve> {
    if data.len() < 8 {
        return None;
    }
    let tag_type = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
    let payload = &data[8..];

    match tag_type {
        TYPE_CURV => {
            if payload.len() < 4 {
                return None;
            }
            let count =
                u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]) as usize;
            if count == 0 {
                return Some(IccToneCurve::Gamma(1.0));
            }
            if count == 1 {
                let gamma = if payload.len() >= 6 {
                    read_u8_fixed8(&payload[4..6])
                } else {
                    1.0
                };
                return Some(IccToneCurve::Gamma(gamma));
            }
            let table_len = count * 2;
            if payload.len() < 4 + table_len {
                return None;
            }
            let mut table = Vec::with_capacity(count);
            for i in 0..count {
                let idx = 4 + i * 2;
                let v = u16::from_be_bytes([payload[idx], payload[idx + 1]]);
                table.push(v);
            }
            Some(IccToneCurve::Table(table))
        }
        TYPE_PARA => {
            if payload.len() < 8 {
                return None;
            }
            let func_type = u16::from_be_bytes([payload[4], payload[5]]);
            let params_start = 8; // reserved 4 + func_type 2 + reserved_2 2
            let params = match func_type {
                0 => {
                    if payload.len() < 8 {
                        vec![1.0]
                    } else {
                        let g = read_s15_fixed16(&payload[params_start..params_start + 4]);
                        vec![g]
                    }
                }
                1 => {
                    if payload.len() < 16 {
                        vec![1.0, 0.0, 0.0]
                    } else {
                        vec![
                            read_s15_fixed16(&payload[params_start..params_start + 4]),
                            read_s15_fixed16(&payload[params_start + 4..params_start + 8]),
                            read_s15_fixed16(&payload[params_start + 8..params_start + 12]),
                        ]
                    }
                }
                2 | 3 => {
                    if payload.len() < 20 {
                        vec![1.0, 0.0, 0.0, 0.0]
                    } else {
                        vec![
                            read_s15_fixed16(&payload[params_start..params_start + 4]),
                            read_s15_fixed16(&payload[params_start + 4..params_start + 8]),
                            read_s15_fixed16(&payload[params_start + 8..params_start + 12]),
                            read_s15_fixed16(&payload[params_start + 12..params_start + 16]),
                        ]
                    }
                }
                4 => {
                    if payload.len() < 28 {
                        vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
                    } else {
                        vec![
                            read_s15_fixed16(&payload[params_start..params_start + 4]),
                            read_s15_fixed16(&payload[params_start + 4..params_start + 8]),
                            read_s15_fixed16(&payload[params_start + 8..params_start + 12]),
                            read_s15_fixed16(&payload[params_start + 12..params_start + 16]),
                            read_s15_fixed16(&payload[params_start + 16..params_start + 20]),
                            read_s15_fixed16(&payload[params_start + 20..params_start + 24]),
                            read_s15_fixed16(&payload[params_start + 24..params_start + 28]),
                        ]
                    }
                }
                _ => vec![1.0],
            };
            Some(IccToneCurve::Parametric(params))
        }
        _ => None,
    }
}

// ============================================================================
// Color conversion
// ============================================================================

/// Apply tone curve: input [0,1] -> output [0,1]
fn apply_tone_curve(curve: &IccToneCurve, x: f64) -> f64 {
    let x = x.clamp(0.0, 1.0);
    match curve {
        IccToneCurve::Gamma(g) => {
            if *g <= 0.0 {
                1.0
            } else {
                x.powf(*g)
            }
        }
        IccToneCurve::Parametric(params) => {
            if params.is_empty() {
                return x;
            }
            let g = params[0];
            if params.len() == 1 {
                return x.powf(g);
            }
            if params.len() >= 4 {
                let a = params[1];
                let b = params[2];
                let c = params[3];
                if x >= -b / a {
                    (a * x + b).powf(g) + c
                } else {
                    c
                }
            } else {
                x.powf(g)
            }
        }
        IccToneCurve::Table(table) => {
            if table.is_empty() {
                return x;
            }
            if table.len() == 1 {
                return (table[0] as f64) / 65535.0;
            }
            let n = table.len() - 1;
            let idx_f = x * (n as f64);
            let idx = idx_f.floor() as usize;
            let frac = idx_f - (idx as f64);
            let idx = idx.min(n - 1);
            let v0 = table[idx] as f64 / 65535.0;
            let v1 = table[idx + 1] as f64 / 65535.0;
            v0 + frac * (v1 - v0)
        }
    }
}

/// Convert RGB [0,1] through ICC profile to PCS XYZ
pub fn icc_to_xyz(profile: &IccProfile, r: f64, g: f64, b: f64) -> Result<[f64; 3]> {
    if profile.header.color_space != IccColorSpace::RGB {
        return Err(Error::unsupported(
            "icc_to_xyz only supports RGB profile colorspace",
        ));
    }

    let curves = profile
        .rgb_tone_curves()
        .ok_or_else(|| Error::format("RGB profile missing rTRC/gTRC/bTRC tags"))?;

    let r_lin = apply_tone_curve(&curves[0], r);
    let g_lin = apply_tone_curve(&curves[1], g);
    let b_lin = apply_tone_curve(&curves[2], b);

    let matrix = profile
        .rgb_primaries()
        .ok_or_else(|| Error::format("RGB profile missing rXYZ/gXYZ/bXYZ tags"))?;

    let x = matrix[0][0] * r_lin + matrix[1][0] * g_lin + matrix[2][0] * b_lin;
    let y = matrix[0][1] * r_lin + matrix[1][1] * g_lin + matrix[2][1] * b_lin;
    let z = matrix[0][2] * r_lin + matrix[1][2] * g_lin + matrix[2][2] * b_lin;

    Ok([x, y, z])
}

/// Bradford chromatic adaptation: D50 → D65 (ICC PCS uses D50, sRGB uses D65)
const BRADFORD_D50_TO_D65: [[f64; 3]; 3] = [
    [0.9555766, -0.0230393, 0.0631636],
    [-0.0282895, 1.0099416, 0.0210077],
    [0.0122982, -0.0204830, 1.3299098],
];

/// Convert PCS XYZ (D50) to sRGB [0,1]
/// ICC PCS is always D50; sRGB is D65. Bradford adaptation is applied.
pub fn xyz_to_srgb(x: f64, y: f64, z: f64) -> [f64; 3] {
    let x_d65 = BRADFORD_D50_TO_D65[0][0] * x
        + BRADFORD_D50_TO_D65[0][1] * y
        + BRADFORD_D50_TO_D65[0][2] * z;
    let y_d65 = BRADFORD_D50_TO_D65[1][0] * x
        + BRADFORD_D50_TO_D65[1][1] * y
        + BRADFORD_D50_TO_D65[1][2] * z;
    let z_d65 = BRADFORD_D50_TO_D65[2][0] * x
        + BRADFORD_D50_TO_D65[2][1] * y
        + BRADFORD_D50_TO_D65[2][2] * z;

    let r_lin = 3.2406255 * x_d65 - 1.537208 * y_d65 - 0.4986286 * z_d65;
    let g_lin = -0.9689307 * x_d65 + 1.8757561 * y_d65 + 0.0415175 * z_d65;
    let b_lin = 0.0557101 * x_d65 - 0.2040211 * y_d65 + 1.0569959 * z_d65;

    fn linear_to_srgb(v: f64) -> f64 {
        let v = v.clamp(0.0, 1.0);
        if v <= 0.0031308 {
            v * 12.92
        } else {
            1.055 * v.powf(1.0 / 2.4) - 0.055
        }
    }

    [
        linear_to_srgb(r_lin),
        linear_to_srgb(g_lin),
        linear_to_srgb(b_lin),
    ]
}

/// Full conversion pipeline: source color -> source ICC -> XYZ -> dest ICC -> dest color
pub fn convert_with_icc(
    src_r: f64,
    src_g: f64,
    src_b: f64,
    src_profile: &IccProfile,
    dst_profile: &IccProfile,
    _intent: RenderingIntent,
) -> Result<[f64; 3]> {
    let xyz = icc_to_xyz(src_profile, src_r, src_g, src_b)?;

    if dst_profile.header.color_space == IccColorSpace::RGB {
        let rgb = xyz_to_srgb(xyz[0], xyz[1], xyz[2]);
        Ok(rgb)
    } else {
        Err(Error::unsupported(
            "convert_with_icc destination must be RGB profile",
        ))
    }
}

// ============================================================================
// IccCache
// ============================================================================

/// Cache for parsed ICC profiles, keyed by data hash
pub struct IccCache {
    cache: RwLock<HashMap<[u8; 32], IccProfile>>,
}

impl IccCache {
    pub fn new() -> Self {
        Self {
            cache: RwLock::new(HashMap::new()),
        }
    }

    pub fn get_or_parse(&self, data: &[u8]) -> Result<IccProfile> {
        use sha2::{Digest, Sha256};
        let mut hasher = Sha256::new();
        hasher.update(data);
        let hash: [u8; 32] = hasher.finalize().into();

        {
            let cache = self
                .cache
                .read()
                .map_err(|e| Error::generic(e.to_string()))?;
            if let Some(profile) = cache.get(&hash) {
                return Ok(profile.clone());
            }
        }

        let profile = parse_icc_profile(data)?;
        {
            let mut cache = self
                .cache
                .write()
                .map_err(|e| Error::generic(e.to_string()))?;
            cache.insert(hash, profile.clone());
        }
        Ok(profile)
    }
}

impl Default for IccCache {
    fn default() -> Self {
        Self::new()
    }
}

// ============================================================================
// Tests
// ============================================================================

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

    fn write_u32_be(buf: &mut Vec<u8>, v: u32) {
        buf.extend_from_slice(&v.to_be_bytes());
    }

    fn write_s15_fixed16(buf: &mut Vec<u8>, v: f64) {
        let raw = (v * 65536.0) as i32;
        buf.extend_from_slice(&raw.to_be_bytes());
    }

    fn write_u8_fixed8(buf: &mut Vec<u8>, v: f64) {
        let raw = (v * 256.0) as u16;
        buf.extend_from_slice(&raw.to_be_bytes());
    }

    /// Build minimal sRGB-like ICC profile for testing
    fn build_minimal_srgb_icc() -> Vec<u8> {
        let mut data = Vec::new();

        // Header 128 bytes
        let profile_size = 332u32; // will fix at end
        write_u32_be(&mut data, profile_size);
        write_u32_be(&mut data, 0); // preferred CMM
        data.extend_from_slice(&[0x04, 0x00, 0x00, 0x00]); // version 4.0
        data.extend_from_slice(b"mntr"); // display
        data.extend_from_slice(b"RGB "); // RGB
        data.extend_from_slice(b"XYZ "); // PCS
        data.extend_from_slice(&[0u8; 12]); // creation date
        data.extend_from_slice(b"acsp");
        data.extend_from_slice(&[0u8; 4]); // primary platform
        data.extend_from_slice(&[0u8; 4]); // flags
        data.extend_from_slice(&[0u8; 4]); // device mfg
        data.extend_from_slice(&[0u8; 4]); // device model
        data.extend_from_slice(&[0u8; 8]); // device attributes
        data.extend_from_slice(&[0u8; 4]); // rendering intent
        // Illuminant D50 (PCS)
        write_s15_fixed16(&mut data, 0.9642);
        write_s15_fixed16(&mut data, 1.0);
        write_s15_fixed16(&mut data, 0.8249);
        data.extend_from_slice(&[0u8; 4]); // creator
        data.extend_from_slice(&[0u8; 16]); // identifier
        data.extend_from_slice(&[0u8; 28]); // reserved

        assert_eq!(data.len(), 128);

        // Build tag data first
        let mut rxyz = Vec::new();
        rxyz.extend_from_slice(b"XYZ ");
        rxyz.extend_from_slice(&[0u8; 4]);
        write_s15_fixed16(&mut rxyz, 0.43606567);
        write_s15_fixed16(&mut rxyz, 0.22248840);
        write_s15_fixed16(&mut rxyz, 0.01391608);

        let mut gxyz = Vec::new();
        gxyz.extend_from_slice(b"XYZ ");
        gxyz.extend_from_slice(&[0u8; 4]);
        write_s15_fixed16(&mut gxyz, 0.38514793);
        write_s15_fixed16(&mut gxyz, 0.71687317);
        write_s15_fixed16(&mut gxyz, 0.09707642);

        let mut bxyz = Vec::new();
        bxyz.extend_from_slice(b"XYZ ");
        bxyz.extend_from_slice(&[0u8; 4]);
        write_s15_fixed16(&mut bxyz, 0.14306641);
        write_s15_fixed16(&mut bxyz, 0.06060742);
        write_s15_fixed16(&mut bxyz, 0.71409607);

        let mut rtrc = Vec::new();
        rtrc.extend_from_slice(b"curv");
        rtrc.extend_from_slice(&[0u8; 4]);
        write_u32_be(&mut rtrc, 1);
        write_u8_fixed8(&mut rtrc, 2.4);

        let mut gtrc = rtrc.clone();
        let mut btrc = rtrc.clone();

        let mut wtpt = Vec::new();
        wtpt.extend_from_slice(b"XYZ ");
        wtpt.extend_from_slice(&[0u8; 4]);
        write_s15_fixed16(&mut wtpt, 0.9642);
        write_s15_fixed16(&mut wtpt, 1.0);
        write_s15_fixed16(&mut wtpt, 0.8249);

        let mut desc = Vec::new();
        desc.extend_from_slice(b"desc");
        desc.extend_from_slice(&[0u8; 4]);
        write_u32_be(&mut desc, 0);
        write_u32_be(&mut desc, 4);
        desc.extend_from_slice(b"sRGB");

        let tag_count = 8u32;
        let tag_table_size = 4 + 12 * tag_count;
        let base_offset = 128 + tag_table_size;

        let mut offset = base_offset;
        let mut entries: Vec<(u32, u32, u32)> = Vec::new();

        for (sig, tag_data) in [
            (TAG_RXYZ, &rxyz),
            (TAG_GXYZ, &gxyz),
            (TAG_BXYZ, &bxyz),
            (TAG_RTRC, &rtrc),
            (TAG_GTRC, &gtrc),
            (TAG_BTRC, &btrc),
            (TAG_WTPT, &wtpt),
            (TAG_DESC, &desc),
        ] {
            entries.push((sig, offset, tag_data.len() as u32));
            offset += tag_data.len() as u32;
        }

        write_u32_be(&mut data, tag_count);
        for (sig, off, size) in &entries {
            write_u32_be(&mut data, *sig);
            write_u32_be(&mut data, *off);
            write_u32_be(&mut data, *size);
        }

        data.extend_from_slice(&rxyz);
        data.extend_from_slice(&gxyz);
        data.extend_from_slice(&bxyz);
        data.extend_from_slice(&rtrc);
        data.extend_from_slice(&gtrc);
        data.extend_from_slice(&btrc);
        data.extend_from_slice(&wtpt);
        data.extend_from_slice(&desc);

        let total_size = data.len() as u32;
        data[0..4].copy_from_slice(&total_size.to_be_bytes());

        data
    }

    #[test]
    fn test_icc_header_parsing() {
        let data = build_minimal_srgb_icc();
        let profile = parse_icc_profile(&data).unwrap();
        assert_eq!(profile.header.size, data.len() as u32);
        assert_eq!(profile.header.version_major, 4);
        assert_eq!(profile.header.device_class, IccDeviceClass::Display);
        assert_eq!(profile.header.color_space, IccColorSpace::RGB);
        assert_eq!(profile.header.rendering_intent, RenderingIntent::Perceptual);
    }

    #[test]
    fn test_icc_invalid_signature() {
        let mut data = vec![0u8; 128];
        data[0..4].copy_from_slice(&128u32.to_be_bytes());
        data[36..40].copy_from_slice(b"xxxx");
        let result = parse_icc_profile(&data);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("acsp"));
    }

    #[test]
    fn test_icc_too_short() {
        let result = parse_icc_profile(&[0u8; 64]);
        assert!(result.is_err());
    }

    #[test]
    fn test_tone_curve_gamma() {
        let mut data = Vec::new();
        data.extend_from_slice(b"curv");
        data.extend_from_slice(&[0u8; 4]);
        write_u32_be(&mut data, 1);
        write_u8_fixed8(&mut data, 2.2);
        let curve = parse_tone_curve(&data).unwrap();
        assert!(matches!(curve, IccToneCurve::Gamma(g) if (g - 2.2).abs() < 0.01));
    }

    #[test]
    fn test_tone_curve_table() {
        let mut data = Vec::new();
        data.extend_from_slice(b"curv");
        data.extend_from_slice(&[0u8; 4]);
        write_u32_be(&mut data, 3);
        data.extend_from_slice(&0u16.to_be_bytes());
        data.extend_from_slice(&32768u16.to_be_bytes());
        data.extend_from_slice(&65535u16.to_be_bytes());
        let curve = parse_tone_curve(&data).unwrap();
        assert!(matches!(curve, IccToneCurve::Table(t) if t.len() == 3));
    }

    #[test]
    fn test_rgb_to_xyz_conversion() {
        let data = build_minimal_srgb_icc();
        let profile = parse_icc_profile(&data).unwrap();
        let xyz = icc_to_xyz(&profile, 1.0, 1.0, 1.0).unwrap();
        assert!(
            xyz[0] > 0.9 && xyz[1] > 0.9 && xyz[2] > 0.7,
            "white XYZ: {:?}",
            xyz
        );
        let xyz_black = icc_to_xyz(&profile, 0.0, 0.0, 0.0).unwrap();
        assert!(
            xyz_black[0].abs() < 0.01 && xyz_black[1].abs() < 0.01 && xyz_black[2].abs() < 0.01
        );
    }

    #[test]
    fn test_xyz_to_srgb_conversion() {
        let data = build_minimal_srgb_icc();
        let profile = parse_icc_profile(&data).unwrap();
        let xyz = icc_to_xyz(&profile, 1.0, 0.0, 0.0).unwrap();
        let rgb = xyz_to_srgb(xyz[0], xyz[1], xyz[2]);
        assert!(rgb[0] > 0.9);
        assert!(rgb[1] < 0.2);
        assert!(rgb[2] < 0.2);
    }

    #[test]
    fn test_srgb_round_trip() {
        let data = build_minimal_srgb_icc();
        let profile = parse_icc_profile(&data).unwrap();
        let src = [0.5, 0.0, 0.0];
        let xyz = icc_to_xyz(&profile, src[0], src[1], src[2]).unwrap();
        let rgb = xyz_to_srgb(xyz[0], xyz[1], xyz[2]);
        let back = icc_to_xyz(&profile, rgb[0], rgb[1], rgb[2]).unwrap();
        assert!((xyz[0] - back[0]).abs() < 0.02);
        assert!((xyz[1] - back[1]).abs() < 0.02);
        assert!((xyz[2] - back[2]).abs() < 0.02);
    }

    #[test]
    fn test_icc_cache() {
        let cache = IccCache::new();
        let data = build_minimal_srgb_icc();
        let p1 = cache.get_or_parse(&data).unwrap();
        let p2 = cache.get_or_parse(&data).unwrap();
        assert_eq!(p1.header.size, p2.header.size);
    }

    #[test]
    fn test_convert_with_icc() {
        let data = build_minimal_srgb_icc();
        let profile = parse_icc_profile(&data).unwrap();
        let result = convert_with_icc(
            1.0,
            0.0,
            0.0,
            &profile,
            &profile,
            RenderingIntent::Perceptual,
        )
        .unwrap();
        assert!(result[0] > 0.9);
        assert!(result[1] < 0.2);
        assert!(result[2] < 0.2);
    }
}
