//! PDF colorspace module
//!
//! Handles PDF color space definitions and color conversion between spaces.

use crate::fitz::error::{Error, Result};
use crate::pdf::object::{Dict, Name, Object, PdfString};
use smallvec::SmallVec;
use std::collections::HashMap;

// ============================================================================
// Helpers
// ============================================================================

/// Extract numeric value from PDF object, supporting both Int and Real types.
fn get_number(obj: &Object) -> Option<f64> {
    match obj {
        Object::Int(i) => Some(*i as f64),
        Object::Real(r) => Some(*r),
        _ => obj.as_real().or_else(|| obj.as_int().map(|i| i as f64)),
    }
}

/// Get colorspace dictionary from array at index. Returns the dict if present and valid,
/// or empty_ref if the element is missing. Returns error if element exists but is not a dict.
fn get_colorspace_dict<'a>(
    arr: &'a [Object],
    idx: usize,
    empty_ref: &'a Dict,
    cs_name: &str,
) -> Result<&'a Dict> {
    match arr.get(idx) {
        None => Ok(empty_ref),
        Some(obj) => obj.as_dict().ok_or_else(|| {
            Error::pdf(format!(
                "{} colorspace requires dictionary at index {}, got invalid type",
                cs_name, idx
            ))
        }),
    }
}

/// PDF color space type definitions
#[derive(Debug, Clone, PartialEq)]
pub enum ColorSpaceType {
    /// Single component [0..1]
    DeviceGray,
    /// Three components [0..1] each
    DeviceRGB,
    /// Four components [0..1] each
    DeviceCMYK,
    /// Calibrated gray with white point, black point, and gamma
    CalGray {
        white_point: [f64; 3],
        black_point: [f64; 3],
        gamma: f64,
    },
    /// Calibrated RGB with white point, black point, gamma, and matrix
    CalRGB {
        white_point: [f64; 3],
        black_point: [f64; 3],
        gamma: [f64; 3],
        matrix: [f64; 9],
    },
    /// CIE L*a*b* with white point, black point, and range
    Lab {
        white_point: [f64; 3],
        black_point: [f64; 3],
        range: [f64; 4],
    },
    /// ICC profile based
    ICCBased {
        num_components: u8,
        alternate: Box<ColorSpace>,
        profile_data: Vec<u8>,
    },
    /// Indexed color lookup table
    Indexed {
        base: Box<ColorSpace>,
        hival: u8,
        lookup: Vec<u8>,
    },
    /// Spot color separation.
    ///
    /// Tint transform evaluation is not implemented. Color conversion falls back
    /// to the alternate colorspace; a warning is emitted when this occurs.
    Separation {
        name: String,
        alternate: Box<ColorSpace>,
    },
    /// DeviceN multiple spot colors.
    ///
    /// Tint transform evaluation is not implemented. Color conversion falls back
    /// to the alternate colorspace; a warning is emitted when this occurs.
    DeviceN {
        names: Vec<String>,
        alternate: Box<ColorSpace>,
    },
    /// Pattern color space (no direct color conversion)
    Pattern,
}

// ============================================================================
// ColorSpace
// ============================================================================

/// PDF color space
#[derive(Debug, Clone)]
pub struct ColorSpace {
    pub(crate) cs_type: ColorSpaceType,
}

impl PartialEq for ColorSpace {
    fn eq(&self, other: &Self) -> bool {
        self.cs_type == other.cs_type
    }
}

impl ColorSpace {
    pub fn device_gray() -> Self {
        Self {
            cs_type: ColorSpaceType::DeviceGray,
        }
    }

    pub fn device_rgb() -> Self {
        Self {
            cs_type: ColorSpaceType::DeviceRGB,
        }
    }

    pub fn device_cmyk() -> Self {
        Self {
            cs_type: ColorSpaceType::DeviceCMYK,
        }
    }

    pub fn num_components(&self) -> usize {
        match &self.cs_type {
            ColorSpaceType::DeviceGray => 1,
            ColorSpaceType::DeviceRGB => 3,
            ColorSpaceType::DeviceCMYK => 4,
            ColorSpaceType::CalGray { .. } => 1,
            ColorSpaceType::CalRGB { .. } => 3,
            ColorSpaceType::Lab { .. } => 3,
            ColorSpaceType::ICCBased { num_components, .. } => *num_components as usize,
            ColorSpaceType::Indexed { .. } => 1,
            ColorSpaceType::Separation { .. } => 1,
            ColorSpaceType::DeviceN { names, .. } => names.len(),
            ColorSpaceType::Pattern => 0,
        }
    }

    pub fn name(&self) -> &str {
        match &self.cs_type {
            ColorSpaceType::DeviceGray => "DeviceGray",
            ColorSpaceType::DeviceRGB => "DeviceRGB",
            ColorSpaceType::DeviceCMYK => "DeviceCMYK",
            ColorSpaceType::CalGray { .. } => "CalGray",
            ColorSpaceType::CalRGB { .. } => "CalRGB",
            ColorSpaceType::Lab { .. } => "Lab",
            ColorSpaceType::ICCBased { .. } => "ICCBased",
            ColorSpaceType::Indexed { .. } => "Indexed",
            ColorSpaceType::Separation { name, .. } => name,
            ColorSpaceType::DeviceN { .. } => "DeviceN",
            ColorSpaceType::Pattern => "Pattern",
        }
    }

    /// Parse colorspace from PDF object. Indirect references must be resolved before calling.
    pub fn from_pdf_object(obj: &Object) -> Result<Self> {
        match obj {
            Object::Ref(_) => Err(Error::pdf(
                "indirect reference in colorspace - resolve before parsing",
            )),
            Object::Name(n) => {
                let s = n.as_str();
                match s {
                    "DeviceGray" | "G" => Ok(Self::device_gray()),
                    "DeviceRGB" | "RGB" => Ok(Self::device_rgb()),
                    "DeviceCMYK" | "CMYK" => Ok(Self::device_cmyk()),
                    "Pattern" => Ok(Self {
                        cs_type: ColorSpaceType::Pattern,
                    }),
                    _ => Err(Error::pdf(format!("unknown colorspace name: {}", s))),
                }
            }
            Object::Array(arr) if !arr.is_empty() => {
                let name_obj = arr[0]
                    .as_name()
                    .ok_or_else(|| Error::pdf("colorspace array first element must be a name"))?;
                let name = name_obj.as_str();

                let empty = HashMap::new();
                match name {
                    "CalGray" => {
                        let dict = get_colorspace_dict(&arr, 1, &empty, "CalGray")?;
                        let (white_point, black_point, gamma) = parse_cal_common(dict)?;
                        Ok(Self {
                            cs_type: ColorSpaceType::CalGray {
                                white_point,
                                black_point,
                                gamma,
                            },
                        })
                    }
                    "CalRGB" => {
                        let dict = get_colorspace_dict(&arr, 1, &empty, "CalRGB")?;
                        let (white_point, black_point, gamma_arr) = parse_cal_rgb_common(dict)?;
                        let matrix = parse_matrix(dict)?;
                        Ok(Self {
                            cs_type: ColorSpaceType::CalRGB {
                                white_point,
                                black_point,
                                gamma: gamma_arr,
                                matrix,
                            },
                        })
                    }
                    "Lab" => {
                        let dict = get_colorspace_dict(&arr, 1, &empty, "Lab")?;
                        let (white_point, black_point) = parse_lab_common(dict)?;
                        let range = parse_lab_range(dict)?;
                        Ok(Self {
                            cs_type: ColorSpaceType::Lab {
                                white_point,
                                black_point,
                                range,
                            },
                        })
                    }
                    "ICCBased" => {
                        if arr.len() < 2 {
                            return Err(Error::pdf("ICCBased colorspace requires stream"));
                        }
                        let stream_obj = &arr[1];
                        let (dict, data) = match stream_obj {
                            Object::Stream { dict, data } => (dict, data.clone()),
                            Object::Ref(_) => {
                                return Err(Error::pdf(
                                    "ICCBased stream must be resolved before parsing",
                                ));
                            }
                            _ => {
                                return Err(Error::pdf(
                                    "ICCBased colorspace second element must be stream",
                                ));
                            }
                        };
                        let n_f64 = dict
                            .get(&Name::new("N"))
                            .and_then(get_number)
                            .filter(|n| n.is_finite())
                            .ok_or_else(|| {
                                Error::pdf("ICCBased colorspace missing required /N parameter")
                            })?;
                        let n = n_f64.round().clamp(1.0, 4.0) as u8;
                        let num_components = match n {
                            1 => 1u8,
                            3 => 3u8,
                            4 => 4u8,
                            _ => {
                                return Err(Error::pdf(format!(
                                    "ICCBased colorspace N must be 1, 3, or 4, got {}",
                                    n
                                )));
                            }
                        };
                        let alternate = dict
                            .get(&Name::new("Alternate"))
                            .map(|o| Self::from_pdf_object(o))
                            .transpose()?
                            .map(Box::new)
                            .unwrap_or_else(|| {
                                Box::new(match num_components {
                                    1 => Self::device_gray(),
                                    3 => Self::device_rgb(),
                                    4 => Self::device_cmyk(),
                                    _ => unreachable!("N is already validated to 1, 3, or 4"),
                                })
                            });
                        Ok(Self {
                            cs_type: ColorSpaceType::ICCBased {
                                num_components,
                                alternate,
                                profile_data: data,
                            },
                        })
                    }
                    "Indexed" | "I" => {
                        if arr.len() < 4 {
                            return Err(Error::pdf(
                                "Indexed colorspace requires [base hival lookup]",
                            ));
                        }
                        let base = Box::new(Self::from_pdf_object(&arr[1])?);
                        let hival_f64 = get_number(&arr[2]).unwrap_or(255.0);
                        if !hival_f64.is_finite() {
                            return Err(Error::pdf(
                                "Indexed colorspace hival must be finite (rejecting NaN/Infinity)",
                            ));
                        }
                        let hival_i32 = hival_f64.round() as i32;
                        if hival_i32 < 0 || hival_i32 > 255 {
                            return Err(Error::pdf(format!(
                                "Indexed colorspace hival {} out of range [0, 255]",
                                hival_i32
                            )));
                        }
                        let hival = hival_i32 as u8;
                        let (lookup, effective_hival) =
                            parse_indexed_lookup(&arr[3], base.num_components(), hival)?;
                        Ok(Self {
                            cs_type: ColorSpaceType::Indexed {
                                base,
                                hival: effective_hival,
                                lookup,
                            },
                        })
                    }
                    "Separation" => {
                        if arr.len() < 3 {
                            return Err(Error::pdf(
                                "Separation colorspace requires [name alternate tint_transform]",
                            ));
                        }
                        let name = arr[1]
                            .as_name()
                            .map(|n| n.as_str().to_string())
                            .or_else(|| {
                                arr[1]
                                    .as_string()
                                    .and_then(|s| s.as_str().map(String::from))
                            })
                            .unwrap_or_else(|| "Unknown".to_string());
                        let alternate = Box::new(Self::from_pdf_object(&arr[2])?);
                        Ok(Self {
                            cs_type: ColorSpaceType::Separation { name, alternate },
                        })
                    }
                    "DeviceN" => {
                        if arr.len() < 3 {
                            return Err(Error::pdf(
                                "DeviceN colorspace requires [names alternate tint_transform]",
                            ));
                        }
                        let names: Vec<String> = arr[1]
                            .as_array()
                            .map(|a| {
                                a.iter()
                                    .filter_map(|o| {
                                        o.as_name().map(|n| n.as_str().to_string()).or_else(|| {
                                            o.as_string().and_then(|s| s.as_str().map(String::from))
                                        })
                                    })
                                    .collect()
                            })
                            .unwrap_or_default();
                        let alternate = Box::new(Self::from_pdf_object(&arr[2])?);
                        Ok(Self {
                            cs_type: ColorSpaceType::DeviceN { names, alternate },
                        })
                    }
                    "Pattern" => Ok(Self {
                        cs_type: ColorSpaceType::Pattern,
                    }),
                    _ => Err(Error::pdf(format!(
                        "unknown colorspace array type: {}",
                        name
                    ))),
                }
            }
            _ => Err(Error::pdf("colorspace must be name or array")),
        }
    }
}

fn parse_cal_common(dict: &Dict) -> Result<([f64; 3], [f64; 3], f64)> {
    let white_point = dict
        .get(&Name::new("WhitePoint"))
        .and_then(|o| o.as_array())
        .filter(|a| a.len() >= 3)
        .map(|a| {
            [
                a.get(0).and_then(get_number).unwrap_or(1.0),
                a.get(1).and_then(get_number).unwrap_or(1.0),
                a.get(2).and_then(get_number).unwrap_or(1.0),
            ]
        })
        .unwrap_or([1.0, 1.0, 1.0]);
    let black_point = dict
        .get(&Name::new("BlackPoint"))
        .and_then(|o| o.as_array())
        .filter(|a| a.len() >= 3)
        .map(|a| {
            [
                a.get(0).and_then(get_number).unwrap_or(0.0),
                a.get(1).and_then(get_number).unwrap_or(0.0),
                a.get(2).and_then(get_number).unwrap_or(0.0),
            ]
        })
        .unwrap_or([0.0, 0.0, 0.0]);
    let gamma = dict
        .get(&Name::new("Gamma"))
        .and_then(get_number)
        .filter(|&g| g > 0.0)
        .unwrap_or(1.0);
    Ok((white_point, black_point, gamma))
}

fn parse_cal_rgb_common(dict: &Dict) -> Result<([f64; 3], [f64; 3], [f64; 3])> {
    let (white_point, black_point, gamma_single) = parse_cal_common(dict)?;
    let gamma = dict
        .get(&Name::new("Gamma"))
        .and_then(|o| o.as_array())
        .filter(|a| a.len() >= 3)
        .map(|a| {
            [
                a.get(0)
                    .and_then(get_number)
                    .filter(|&g| g > 0.0)
                    .unwrap_or(1.0),
                a.get(1)
                    .and_then(get_number)
                    .filter(|&g| g > 0.0)
                    .unwrap_or(1.0),
                a.get(2)
                    .and_then(get_number)
                    .filter(|&g| g > 0.0)
                    .unwrap_or(1.0),
            ]
        })
        .unwrap_or([gamma_single, gamma_single, gamma_single]);
    Ok((white_point, black_point, gamma))
}

fn parse_matrix(dict: &Dict) -> Result<[f64; 9]> {
    Ok(dict
        .get(&Name::new("Matrix"))
        .and_then(|o| o.as_array())
        .filter(|a| a.len() >= 9)
        .map(|a| {
            [
                a.get(0).and_then(get_number).unwrap_or(1.0),
                a.get(1).and_then(get_number).unwrap_or(0.0),
                a.get(2).and_then(get_number).unwrap_or(0.0),
                a.get(3).and_then(get_number).unwrap_or(0.0),
                a.get(4).and_then(get_number).unwrap_or(1.0),
                a.get(5).and_then(get_number).unwrap_or(0.0),
                a.get(6).and_then(get_number).unwrap_or(0.0),
                a.get(7).and_then(get_number).unwrap_or(0.0),
                a.get(8).and_then(get_number).unwrap_or(1.0),
            ]
        })
        .unwrap_or([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]))
}

fn parse_lab_common(dict: &Dict) -> Result<([f64; 3], [f64; 3])> {
    let (white_point, black_point, _) = parse_cal_common(dict)?;
    Ok((white_point, black_point))
}

fn parse_lab_range(dict: &Dict) -> Result<[f64; 4]> {
    Ok(dict
        .get(&Name::new("Range"))
        .and_then(|o| o.as_array())
        .filter(|a| a.len() >= 4)
        .map(|a| {
            [
                a.get(0).and_then(get_number).unwrap_or(-100.0),
                a.get(1).and_then(get_number).unwrap_or(100.0),
                a.get(2).and_then(get_number).unwrap_or(-100.0),
                a.get(3).and_then(get_number).unwrap_or(100.0),
            ]
        })
        .unwrap_or([-100.0, 100.0, -100.0, 100.0]))
}

/// Parse Indexed colorspace lookup table.
///
/// Returns `(lookup_data, effective_hival)`. The lookup table is stored as-is without
/// zero-padding. If the source data is shorter than the expected size
/// `(hival + 1) * base_components`, the effective hival is clamped to the number of
/// complete entries that fit in the available data, and this limitation is documented.
fn parse_indexed_lookup(obj: &Object, base_components: usize, hival: u8) -> Result<(Vec<u8>, u8)> {
    if base_components == 0 {
        return Err(Error::pdf("Indexed base colorspace has zero components"));
    }
    let entries = (hival as usize)
        .checked_add(1)
        .ok_or_else(|| Error::pdf("Indexed colorspace hival overflow"))?;
    let expected_len = entries
        .checked_mul(base_components)
        .ok_or_else(|| Error::pdf("Indexed lookup table size overflow"))?;

    let lookup: Vec<u8> = match obj {
        Object::String(_) => {
            let s = obj
                .as_string()
                .ok_or_else(|| Error::pdf("Indexed lookup must be string or stream"))?;
            s.as_bytes().to_vec()
        }
        Object::Stream { data, .. } => data.clone(),
        Object::Ref(_) => {
            return Err(Error::pdf(
                "Indexed lookup stream must be resolved before parsing",
            ));
        }
        _ => return Err(Error::pdf("Indexed lookup must be string or stream")),
    };

    if lookup.is_empty() {
        return Err(Error::pdf("Indexed colorspace has empty lookup table"));
    }

    let effective_hival = if lookup.len() >= expected_len {
        hival
    } else {
        let complete_entries = lookup.len() / base_components;
        let effective = complete_entries.saturating_sub(1);
        effective.min(255) as u8
    };

    Ok((lookup, effective_hival))
}

// ============================================================================
// Color
// ============================================================================

/// PDF color with component values
#[derive(Debug, Clone)]
pub struct Color {
    pub components: SmallVec<[f64; 4]>,
}

impl Color {
    /// Create color from components. For device spaces use [0,1]; for Lab use L in [0,100].
    pub fn new(components: &[f64]) -> Self {
        Self {
            components: components.iter().copied().collect(),
        }
    }

    pub fn gray(g: f64) -> Self {
        Self::new(&[clamp_component(g)])
    }

    pub fn rgb(r: f64, g: f64, b: f64) -> Self {
        Self::new(&[clamp_component(r), clamp_component(g), clamp_component(b)])
    }

    pub fn cmyk(c: f64, m: f64, y: f64, k: f64) -> Self {
        Self::new(&[
            clamp_component(c),
            clamp_component(m),
            clamp_component(y),
            clamp_component(k),
        ])
    }
}

// ============================================================================
// Utility
// ============================================================================

/// Clamp color component to [0.0, 1.0]
pub fn clamp_component(v: f64) -> f64 {
    v.clamp(0.0, 1.0)
}

// ============================================================================
// Color Conversion
// ============================================================================

/// Maximum recursion depth for ICCBased → alternate → ICCBased chains.
const MAX_CONVERT_DEPTH: usize = 10;

/// Convert color from one colorspace to another
pub fn convert_color(color: &Color, from: &ColorSpace, to: &ColorSpace) -> Result<Color> {
    if from == to {
        return Ok(color.clone());
    }

    // Pattern cannot be converted
    if matches!(to.cs_type, ColorSpaceType::Pattern)
        || matches!(from.cs_type, ColorSpaceType::Pattern)
    {
        return Err(Error::unsupported(
            "cannot convert to or from Pattern colorspace",
        ));
    }

    // Resolve alternate for ICCBased, Separation, DeviceN
    let from_base = resolve_base_space(from);
    let to_base = resolve_base_space(to);

    // Indexed: lookup then convert base
    let from_color = if let ColorSpaceType::Indexed {
        base,
        hival,
        lookup,
    } = &from_base.cs_type
    {
        let base_n = base.num_components();
        if base_n == 0 {
            return Err(Error::pdf("Indexed base colorspace has zero components"));
        }
        if lookup.is_empty() {
            return Err(Error::pdf("Indexed colorspace has empty lookup table"));
        }
        let comp = color.components.get(0).copied().unwrap_or(0.0);
        if !comp.is_finite() {
            return Err(Error::pdf(
                "NaN or Infinity in color component during indexed lookup",
            ));
        }
        let comp = comp.clamp(0.0, 1.0);
        let hival_usize = *hival as usize;
        let index_f64 = comp * (hival_usize as f64);
        let index_f64 = if index_f64.is_finite() {
            index_f64.round().clamp(0.0, hival_usize as f64)
        } else {
            0.0
        };
        let index = index_f64 as usize;
        let max_valid_index = (*hival as usize).min((lookup.len() / base_n).saturating_sub(1));
        let index = index.min(max_valid_index);
        let base_idx = index
            .checked_mul(base_n)
            .ok_or_else(|| Error::pdf("indexed colorspace lookup index overflow"))?;
        let end_idx = base_idx
            .checked_add(base_n)
            .ok_or_else(|| Error::pdf("indexed colorspace lookup offset overflow"))?;
        if end_idx > lookup.len() {
            return Err(Error::pdf(
                "indexed colorspace lookup table truncated - index exceeds available data",
            ));
        }
        let slice = &lookup[base_idx..end_idx];
        let base_components: Vec<f64> = slice.iter().map(|&b| b as f64 / 255.0).collect();
        Color::new(&base_components)
    } else {
        color.clone()
    };

    let from_cs = if matches!(from.cs_type, ColorSpaceType::Indexed { .. }) {
        match &from_base.cs_type {
            ColorSpaceType::Indexed { base, .. } => base.as_ref(),
            _ => &from_base,
        }
    } else {
        &from_base
    };

    let to_cs = if matches!(to.cs_type, ColorSpaceType::Indexed { .. }) {
        match &to_base.cs_type {
            ColorSpaceType::Indexed { base, .. } => base.as_ref(),
            _ => &to_base,
        }
    } else {
        &to_base
    };

    convert_between_device_spaces_impl(&from_color, from_cs, to_cs, 0)
}

fn resolve_base_space(cs: &ColorSpace) -> ColorSpace {
    match &cs.cs_type {
        ColorSpaceType::ICCBased { alternate, .. } => (**alternate).clone(),
        ColorSpaceType::Separation { alternate, .. } => (**alternate).clone(),
        ColorSpaceType::DeviceN { alternate, .. } => (**alternate).clone(),
        _ => cs.clone(),
    }
}

fn convert_between_device_spaces_impl(
    color: &Color,
    from: &ColorSpace,
    to: &ColorSpace,
    depth: usize,
) -> Result<Color> {
    if depth >= MAX_CONVERT_DEPTH {
        return Err(Error::pdf(
            "colorspace conversion exceeded maximum recursion depth (ICCBased/alternate chain)",
        ));
    }

    let from_components = &color.components;

    // Convert from source to DeviceRGB (intermediate)
    let rgb = match &from.cs_type {
        ColorSpaceType::DeviceGray => {
            let g = from_components.get(0).copied().unwrap_or(0.0);
            [g, g, g]
        }
        ColorSpaceType::DeviceRGB => [
            from_components.get(0).copied().unwrap_or(0.0),
            from_components.get(1).copied().unwrap_or(0.0),
            from_components.get(2).copied().unwrap_or(0.0),
        ],
        ColorSpaceType::DeviceCMYK => {
            let c = from_components.get(0).copied().unwrap_or(0.0);
            let m = from_components.get(1).copied().unwrap_or(0.0);
            let y = from_components.get(2).copied().unwrap_or(0.0);
            let k = from_components.get(3).copied().unwrap_or(0.0);
            [
                (1.0 - c) * (1.0 - k),
                (1.0 - m) * (1.0 - k),
                (1.0 - y) * (1.0 - k),
            ]
        }
        ColorSpaceType::CalGray { gamma, .. } => {
            let g = from_components.get(0).copied().unwrap_or(0.0);
            let corrected = g.powf(1.0 / gamma);
            [corrected, corrected, corrected]
        }
        ColorSpaceType::CalRGB { gamma, matrix, .. } => {
            let r = from_components
                .get(0)
                .copied()
                .unwrap_or(0.0)
                .powf(1.0 / gamma[0]);
            let g = from_components
                .get(1)
                .copied()
                .unwrap_or(0.0)
                .powf(1.0 / gamma[1]);
            let b = from_components
                .get(2)
                .copied()
                .unwrap_or(0.0)
                .powf(1.0 / gamma[2]);
            let linear = [
                matrix[0] * r + matrix[1] * g + matrix[2] * b,
                matrix[3] * r + matrix[4] * g + matrix[5] * b,
                matrix[6] * r + matrix[7] * g + matrix[8] * b,
            ];
            [
                linear_to_srgb(linear[0]),
                linear_to_srgb(linear[1]),
                linear_to_srgb(linear[2]),
            ]
        }
        ColorSpaceType::Lab { white_point, .. } => {
            let l = from_components.get(0).copied().unwrap_or(0.0);
            let a = from_components.get(1).copied().unwrap_or(0.0);
            let b = from_components.get(2).copied().unwrap_or(0.0);
            lab_to_rgb(l, a, b, white_point)
        }
        ColorSpaceType::ICCBased { alternate, .. } => {
            return convert_between_device_spaces_impl(color, alternate.as_ref(), to, depth + 1);
        }
        ColorSpaceType::Separation { alternate, .. } => {
            return convert_between_device_spaces_impl(color, alternate.as_ref(), to, depth + 1);
        }
        ColorSpaceType::DeviceN { alternate, .. } => {
            return convert_between_device_spaces_impl(color, alternate.as_ref(), to, depth + 1);
        }
        ColorSpaceType::Indexed { .. } => {
            return Err(Error::pdf("Indexed should be resolved before convert"));
        }
        ColorSpaceType::Pattern => {
            return Err(Error::unsupported("Pattern colorspace conversion"));
        }
    };

    // Convert from DeviceRGB to destination
    let result = match &to.cs_type {
        ColorSpaceType::DeviceGray => {
            let g = 0.3 * rgb[0] + 0.59 * rgb[1] + 0.11 * rgb[2];
            Color::gray(g)
        }
        ColorSpaceType::DeviceRGB => Color::rgb(rgb[0], rgb[1], rgb[2]),
        ColorSpaceType::DeviceCMYK => {
            // RGB to CMYK conversion using the standard K = 1 - max(R,G,B) formula.
            // Approximation only: does not account for dot gain, UCR/GCR, or device
            // profiles. When K is near 1, return pure black to avoid division by
            // near-zero and numerical instability. Divisor is clamped to avoid
            // overflow from tiny (1-K) values.
            let k = 1.0 - rgb[0].max(rgb[1]).max(rgb[2]);
            const NEAR_BLACK_THRESHOLD: f64 = 0.9999;
            const DIVISOR_EPSILON: f64 = 1e-10;
            if k >= NEAR_BLACK_THRESHOLD {
                return Ok(Color::cmyk(0.0, 0.0, 0.0, 1.0));
            }
            let divisor = (1.0 - k).max(DIVISOR_EPSILON);
            let c = ((1.0 - rgb[0] - k) / divisor).clamp(0.0, 1.0);
            let m = ((1.0 - rgb[1] - k) / divisor).clamp(0.0, 1.0);
            let y = ((1.0 - rgb[2] - k) / divisor).clamp(0.0, 1.0);
            Color::cmyk(c, m, y, k)
        }
        ColorSpaceType::CalGray { gamma, .. } => {
            let g = 0.3 * rgb[0] + 0.59 * rgb[1] + 0.11 * rgb[2];
            Color::gray(g.powf(*gamma))
        }
        ColorSpaceType::CalRGB { gamma, .. } => {
            let r = srgb_to_linear(rgb[0]).powf(gamma[0]);
            let g = srgb_to_linear(rgb[1]).powf(gamma[1]);
            let b = srgb_to_linear(rgb[2]).powf(gamma[2]);
            Color::new(&[r, g, b])
        }
        ColorSpaceType::Lab { white_point, .. } => {
            let lab = rgb_to_lab(rgb[0], rgb[1], rgb[2], white_point);
            Color::new(&[lab.0, lab.1, lab.2])
        }
        ColorSpaceType::ICCBased { alternate, .. } => {
            return convert_between_device_spaces_impl(
                &Color::rgb(rgb[0], rgb[1], rgb[2]),
                &ColorSpace::device_rgb(),
                alternate.as_ref(),
                depth + 1,
            );
        }
        ColorSpaceType::Separation { alternate, .. } => {
            return convert_between_device_spaces_impl(
                &Color::rgb(rgb[0], rgb[1], rgb[2]),
                &ColorSpace::device_rgb(),
                alternate.as_ref(),
                depth + 1,
            );
        }
        ColorSpaceType::DeviceN { alternate, .. } => {
            return convert_between_device_spaces_impl(
                &Color::rgb(rgb[0], rgb[1], rgb[2]),
                &ColorSpace::device_rgb(),
                alternate.as_ref(),
                depth + 1,
            );
        }
        ColorSpaceType::Indexed { .. } => {
            return Err(Error::pdf("Indexed destination should be resolved"));
        }
        ColorSpaceType::Pattern => {
            return Err(Error::unsupported("Pattern colorspace conversion"));
        }
    };

    Ok(result)
}

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

fn srgb_to_linear(v: f64) -> f64 {
    let v = clamp_component(v);
    if v <= 0.04045 {
        v / 12.92
    } else {
        ((v + 0.055) / 1.055).powf(2.4)
    }
}

/// Bradford chromatic adaptation matrix: D50 → D65.
/// Lab uses D50 illuminant; sRGB uses D65. This matrix adapts XYZ from D50 to 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],
];

/// Lab to RGB using the colorspace white point and Bradford D50→D65 chromatic adaptation.
/// Lab is defined relative to the white point from the colorspace dictionary.
/// sRGB is defined for D65, so we apply Bradford chromatic adaptation between XYZ and sRGB.
fn lab_to_rgb(l: f64, a: f64, b: f64, white_point: &[f64; 3]) -> [f64; 3] {
    // L* in [0,100], a*, b* typically in [-128,127] or range
    let fy = (l + 16.0) / 116.0;
    let fx = a / 500.0 + fy;
    let fz = fy - b / 200.0;

    let epsilon = 216.0 / 24389.0;
    let kappa = 24389.0 / 27.0;

    let x = if fx.powi(3) > epsilon {
        fx.powi(3)
    } else {
        (116.0 * fx - 16.0) / kappa
    };
    let y = if fy.powi(3) > epsilon {
        fy.powi(3)
    } else {
        (116.0 * fy - 16.0) / kappa
    };
    let z = if fz.powi(3) > epsilon {
        fz.powi(3)
    } else {
        (116.0 * fz - 16.0) / kappa
    };

    // Use actual white point from Lab colorspace dictionary (typically D50)
    let xn = white_point[0];
    let yn = white_point[1];
    let zn = white_point[2];

    let x = x * xn;
    let y = y * yn;
    let z = z * zn;

    // Bradford chromatic adaptation only when white point is D50 (within tolerance).
    // Lab typically uses D50; sRGB uses D65. For non-D50 illuminants, use direct
    // XYZ→sRGB without adaptation (approximate for non-D50).
    let is_d50 =
        (xn - 0.9505).abs() < 0.01 && (yn - 1.0).abs() < 0.01 && (zn - 1.0890).abs() < 0.01;

    let (x_out, y_out, z_out) = if is_d50 {
        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;
        (x_d65, y_d65, z_d65)
    } else {
        // Non-D50 white point: use direct XYZ→sRGB without Bradford adaptation.
        // This is approximate for non-D50 illuminants.
        (x, y, z)
    };

    // XYZ (D65 or direct) to linear RGB (sRGB matrix)
    let r = 3.2406255 * x_out - 1.537208 * y_out - 0.4986286 * z_out;
    let g = -0.9689307 * x_out + 1.8757561 * y_out + 0.0415175 * z_out;
    let b_lin = 0.0557101 * x_out - 0.2040211 * y_out + 1.0569959 * z_out;

    [linear_to_srgb(r), linear_to_srgb(g), linear_to_srgb(b_lin)]
}

/// Bradford chromatic adaptation matrix: D65 → D50 (inverse of D50→D65).
/// Used when converting sRGB (D65) to Lab (D50).
const BRADFORD_D65_TO_D50: [[f64; 3]; 3] = [
    [1.0478112, 0.0228866, -0.0501270],
    [0.0295424, 0.9904844, -0.0170491],
    [-0.0092345, 0.0150436, 0.7521316],
];

/// RGB to Lab using the destination colorspace white point.
/// sRGB is D65; we chromatic-adapt to the Lab white point (typically D50), then convert to Lab.
fn rgb_to_lab(r: f64, g: f64, b: f64, white_point: &[f64; 3]) -> (f64, f64, f64) {
    let r = srgb_to_linear(r);
    let g = srgb_to_linear(g);
    let b = srgb_to_linear(b);

    // Linear sRGB to XYZ (D65)
    let x_d65 = 0.4124564 * r + 0.3575761 * g + 0.1804375 * b;
    let y_d65 = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b;
    let z_d65 = 0.0193339 * r + 0.1191920 * g + 0.9503041 * b;

    // Bradford D65→D50 (Lab typically uses D50)
    let x = BRADFORD_D65_TO_D50[0][0] * x_d65
        + BRADFORD_D65_TO_D50[0][1] * y_d65
        + BRADFORD_D65_TO_D50[0][2] * z_d65;
    let y = BRADFORD_D65_TO_D50[1][0] * x_d65
        + BRADFORD_D65_TO_D50[1][1] * y_d65
        + BRADFORD_D65_TO_D50[1][2] * z_d65;
    let z = BRADFORD_D65_TO_D50[2][0] * x_d65
        + BRADFORD_D65_TO_D50[2][1] * y_d65
        + BRADFORD_D65_TO_D50[2][2] * z_d65;

    // Use actual white point from Lab colorspace dictionary
    let xn = white_point[0];
    let yn = white_point[1];
    let zn = white_point[2];

    let fx = f(x / xn);
    let fy = f(y / yn);
    let fz = f(z / zn);

    let l = 116.0 * fy - 16.0;
    let a = 500.0 * (fx - fy);
    let b_val = 200.0 * (fy - fz);

    (l, a, b_val)
}

fn f(t: f64) -> f64 {
    let delta: f64 = 6.0 / 29.0;
    if t > delta.powi(3) {
        t.cbrt()
    } else {
        t / (3.0 * delta * delta) + 4.0 / 29.0
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::pdf::object::{Dict, Name, ObjRef, Object, PdfString};
    use std::collections::HashMap;

    fn make_dict(entries: &[(&str, Object)]) -> Dict {
        entries
            .iter()
            .map(|(k, v)| (Name::new(k), v.clone()))
            .collect()
    }

    #[test]
    fn test_device_gray() {
        let cs = ColorSpace::device_gray();
        assert_eq!(cs.num_components(), 1);
        assert_eq!(cs.name(), "DeviceGray");
    }

    #[test]
    fn test_device_rgb() {
        let cs = ColorSpace::device_rgb();
        assert_eq!(cs.num_components(), 3);
        assert_eq!(cs.name(), "DeviceRGB");
    }

    #[test]
    fn test_device_cmyk() {
        let cs = ColorSpace::device_cmyk();
        assert_eq!(cs.num_components(), 4);
        assert_eq!(cs.name(), "DeviceCMYK");
    }

    #[test]
    fn test_calgray_creation() {
        let cs = ColorSpace {
            cs_type: ColorSpaceType::CalGray {
                white_point: [1.0, 1.0, 1.0],
                black_point: [0.0, 0.0, 0.0],
                gamma: 2.2,
            },
        };
        assert_eq!(cs.num_components(), 1);
        assert_eq!(cs.name(), "CalGray");
    }

    #[test]
    fn test_calrgb_creation() {
        let cs = ColorSpace {
            cs_type: ColorSpaceType::CalRGB {
                white_point: [1.0, 1.0, 1.0],
                black_point: [0.0, 0.0, 0.0],
                gamma: [2.2, 2.2, 2.2],
                matrix: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
            },
        };
        assert_eq!(cs.num_components(), 3);
        assert_eq!(cs.name(), "CalRGB");
    }

    #[test]
    fn test_lab_creation() {
        let cs = ColorSpace {
            cs_type: ColorSpaceType::Lab {
                white_point: [0.95, 1.0, 1.09],
                black_point: [0.0, 0.0, 0.0],
                range: [-100.0, 100.0, -100.0, 100.0],
            },
        };
        assert_eq!(cs.num_components(), 3);
        assert_eq!(cs.name(), "Lab");
    }

    #[test]
    fn test_indexed_creation() {
        let base = ColorSpace::device_rgb();
        let lookup = vec![0u8; 3 * 256];
        let cs = ColorSpace {
            cs_type: ColorSpaceType::Indexed {
                base: Box::new(base),
                hival: 255,
                lookup,
            },
        };
        assert_eq!(cs.num_components(), 1);
    }

    #[test]
    fn test_parse_name_devicegray() {
        let obj = Object::Name(Name::new("DeviceGray"));
        let cs = ColorSpace::from_pdf_object(&obj).unwrap();
        assert_eq!(cs.num_components(), 1);
        assert_eq!(cs.name(), "DeviceGray");
    }

    #[test]
    fn test_parse_name_devicergb() {
        let obj = Object::Name(Name::new("DeviceRGB"));
        let cs = ColorSpace::from_pdf_object(&obj).unwrap();
        assert_eq!(cs.num_components(), 3);
    }

    #[test]
    fn test_parse_name_devicecmyk() {
        let obj = Object::Name(Name::new("DeviceCMYK"));
        let cs = ColorSpace::from_pdf_object(&obj).unwrap();
        assert_eq!(cs.num_components(), 4);
    }

    #[test]
    fn test_parse_name_pattern() {
        let obj = Object::Name(Name::new("Pattern"));
        let cs = ColorSpace::from_pdf_object(&obj).unwrap();
        assert_eq!(cs.name(), "Pattern");
    }

    #[test]
    fn test_parse_name_g() {
        let obj = Object::Name(Name::new("G"));
        let cs = ColorSpace::from_pdf_object(&obj).unwrap();
        assert_eq!(cs.num_components(), 1);
    }

    #[test]
    fn test_parse_calgray_array() {
        let dict = make_dict(&[
            (
                "WhitePoint",
                Object::Array(vec![
                    Object::Real(1.0),
                    Object::Real(1.0),
                    Object::Real(1.0),
                ]),
            ),
            ("Gamma", Object::Real(2.2)),
        ]);
        let arr = vec![Object::Name(Name::new("CalGray")), Object::Dict(dict)];
        let cs = ColorSpace::from_pdf_object(&Object::Array(arr)).unwrap();
        assert_eq!(cs.num_components(), 1);
        assert_eq!(cs.name(), "CalGray");
    }

    #[test]
    fn test_parse_calrgb_array() {
        let dict = make_dict(&[
            (
                "WhitePoint",
                Object::Array(vec![
                    Object::Real(1.0),
                    Object::Real(1.0),
                    Object::Real(1.0),
                ]),
            ),
            (
                "Matrix",
                Object::Array(vec![
                    Object::Real(1.0),
                    Object::Real(0.0),
                    Object::Real(0.0),
                    Object::Real(0.0),
                    Object::Real(1.0),
                    Object::Real(0.0),
                    Object::Real(0.0),
                    Object::Real(0.0),
                    Object::Real(1.0),
                ]),
            ),
        ]);
        let arr = vec![Object::Name(Name::new("CalRGB")), Object::Dict(dict)];
        let cs = ColorSpace::from_pdf_object(&Object::Array(arr)).unwrap();
        assert_eq!(cs.num_components(), 3);
    }

    #[test]
    fn test_parse_lab_array() {
        let dict = make_dict(&[
            (
                "WhitePoint",
                Object::Array(vec![
                    Object::Real(0.95),
                    Object::Real(1.0),
                    Object::Real(1.09),
                ]),
            ),
            (
                "Range",
                Object::Array(vec![
                    Object::Real(-100.0),
                    Object::Real(100.0),
                    Object::Real(-100.0),
                    Object::Real(100.0),
                ]),
            ),
        ]);
        let arr = vec![Object::Name(Name::new("Lab")), Object::Dict(dict)];
        let cs = ColorSpace::from_pdf_object(&Object::Array(arr)).unwrap();
        assert_eq!(cs.num_components(), 3);
    }

    #[test]
    fn test_parse_indexed_array() {
        let base = Object::Name(Name::new("DeviceRGB"));
        let lookup = vec![0u8; 3 * 4];
        let arr = vec![
            Object::Name(Name::new("Indexed")),
            base,
            Object::Int(3),
            Object::String(PdfString::new(lookup)),
        ];
        let cs = ColorSpace::from_pdf_object(&Object::Array(arr)).unwrap();
        assert_eq!(cs.num_components(), 1);
    }

    #[test]
    fn test_parse_indexed_hival_out_of_range() {
        let base = Object::Name(Name::new("DeviceRGB"));
        let lookup = vec![0u8; 3 * 4];
        let arr = vec![
            Object::Name(Name::new("Indexed")),
            base.clone(),
            Object::Int(256),
            Object::String(PdfString::new(lookup.clone())),
        ];
        let result = ColorSpace::from_pdf_object(&Object::Array(arr));
        assert!(result.is_err());
        assert!(
            result
                .unwrap_err()
                .to_string()
                .contains("hival 256 out of range [0, 255]")
        );

        let arr_neg = vec![
            Object::Name(Name::new("Indexed")),
            base,
            Object::Int(-1),
            Object::String(PdfString::new(lookup)),
        ];
        let result = ColorSpace::from_pdf_object(&Object::Array(arr_neg));
        assert!(result.is_err());
        assert!(
            result
                .unwrap_err()
                .to_string()
                .contains("hival -1 out of range [0, 255]")
        );
    }

    #[test]
    fn test_parse_indexed_empty_lookup() {
        let base = Object::Name(Name::new("DeviceRGB"));
        let arr = vec![
            Object::Name(Name::new("Indexed")),
            base,
            Object::Int(0),
            Object::String(PdfString::new(vec![])),
        ];
        let result = ColorSpace::from_pdf_object(&Object::Array(arr));
        assert!(result.is_err());
        assert!(
            result
                .unwrap_err()
                .to_string()
                .contains("Indexed colorspace has empty lookup table")
        );
    }

    #[test]
    fn test_parse_indexed_short_lookup_clamps_hival() {
        // Lookup shorter than (hival+1)*base_components: 6 bytes for hival=3, base=RGB (3 components)
        // Expected would be 12 bytes. We have 6 -> 2 complete entries -> effective_hival=1
        let base = Object::Name(Name::new("DeviceRGB"));
        let lookup = vec![255u8, 0, 0, 0, 255, 0]; // red at 0, green at 1
        let arr = vec![
            Object::Name(Name::new("Indexed")),
            base,
            Object::Int(3),
            Object::String(PdfString::new(lookup)),
        ];
        let cs = ColorSpace::from_pdf_object(&Object::Array(arr)).unwrap();
        assert_eq!(cs.num_components(), 1);
        if let ColorSpaceType::Indexed {
            hival, lookup: l, ..
        } = &cs.cs_type
        {
            assert_eq!(*hival, 1, "effective hival should be clamped to 1");
            assert_eq!(l.len(), 6, "lookup stored as-is without padding");
        }
    }

    #[test]
    fn test_parse_iccbased_array() {
        let mut dict = Dict::new();
        dict.insert(Name::new("N"), Object::Int(3));
        dict.insert(Name::new("Alternate"), Object::Name(Name::new("DeviceRGB")));
        let stream = Object::Stream {
            dict,
            data: vec![0u8; 128],
        };
        let arr = vec![Object::Name(Name::new("ICCBased")), stream];
        let cs = ColorSpace::from_pdf_object(&Object::Array(arr)).unwrap();
        assert_eq!(cs.num_components(), 3);
    }

    #[test]
    fn test_parse_iccbased_missing_n() {
        let mut dict = Dict::new();
        dict.insert(Name::new("Alternate"), Object::Name(Name::new("DeviceRGB")));
        let stream = Object::Stream {
            dict,
            data: vec![0u8; 128],
        };
        let arr = vec![Object::Name(Name::new("ICCBased")), stream];
        let result = ColorSpace::from_pdf_object(&Object::Array(arr));
        assert!(result.is_err());
        assert!(
            result
                .unwrap_err()
                .to_string()
                .contains("ICCBased colorspace missing required /N parameter")
        );
    }

    #[test]
    fn test_parse_iccbased_invalid_n() {
        let mut dict = Dict::new();
        dict.insert(Name::new("N"), Object::Int(2));
        dict.insert(Name::new("Alternate"), Object::Name(Name::new("DeviceRGB")));
        let stream = Object::Stream {
            dict,
            data: vec![0u8; 128],
        };
        let arr = vec![Object::Name(Name::new("ICCBased")), stream];
        let result = ColorSpace::from_pdf_object(&Object::Array(arr));
        assert!(result.is_err());
        assert!(
            result
                .unwrap_err()
                .to_string()
                .contains("N must be 1, 3, or 4")
        );
    }

    #[test]
    fn test_parse_calgray_invalid_dict() {
        let arr = vec![Object::Name(Name::new("CalGray")), Object::Int(42)];
        let result = ColorSpace::from_pdf_object(&Object::Array(arr));
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("CalGray"));
    }

    #[test]
    fn test_parse_separation_array() {
        let arr = vec![
            Object::Name(Name::new("Separation")),
            Object::Name(Name::new("Cyan")),
            Object::Name(Name::new("DeviceCMYK")),
            Object::Null,
        ];
        let cs = ColorSpace::from_pdf_object(&Object::Array(arr)).unwrap();
        assert_eq!(cs.num_components(), 1);
    }

    #[test]
    fn test_parse_devicen_array() {
        let names = vec![
            Object::Name(Name::new("Cyan")),
            Object::Name(Name::new("Magenta")),
        ];
        let arr = vec![
            Object::Name(Name::new("DeviceN")),
            Object::Array(names),
            Object::Name(Name::new("DeviceCMYK")),
            Object::Null,
        ];
        let cs = ColorSpace::from_pdf_object(&Object::Array(arr)).unwrap();
        assert_eq!(cs.num_components(), 2);
    }

    #[test]
    fn test_color_gray() {
        let c = Color::gray(0.5);
        assert_eq!(c.components.len(), 1);
        assert!((c.components[0] - 0.5).abs() < 1e-10);
    }

    #[test]
    fn test_color_rgb() {
        let c = Color::rgb(1.0, 0.5, 0.0);
        assert_eq!(c.components.len(), 3);
        assert!((c.components[0] - 1.0).abs() < 1e-10);
        assert!((c.components[1] - 0.5).abs() < 1e-10);
        assert!((c.components[2] - 0.0).abs() < 1e-10);
    }

    #[test]
    fn test_color_cmyk() {
        let c = Color::cmyk(0.0, 0.0, 0.0, 1.0);
        assert_eq!(c.components.len(), 4);
        assert!((c.components[3] - 1.0).abs() < 1e-10);
    }

    #[test]
    fn test_color_new() {
        let c = Color::new(&[0.2, 0.4, 0.6]);
        assert_eq!(c.components.len(), 3);
    }

    #[test]
    fn test_clamp_component() {
        assert!((clamp_component(-0.5) - 0.0).abs() < 1e-10);
        assert!((clamp_component(1.5) - 1.0).abs() < 1e-10);
        assert!((clamp_component(0.5) - 0.5).abs() < 1e-10);
    }

    #[test]
    fn test_convert_gray_to_rgb() {
        let gray = ColorSpace::device_gray();
        let rgb = ColorSpace::device_rgb();
        let c = Color::gray(0.5);
        let result = convert_color(&c, &gray, &rgb).unwrap();
        assert_eq!(result.components.len(), 3);
        assert!((result.components[0] - 0.5).abs() < 1e-6);
        assert!((result.components[1] - 0.5).abs() < 1e-6);
        assert!((result.components[2] - 0.5).abs() < 1e-6);
    }

    #[test]
    fn test_convert_gray_to_cmyk() {
        let gray = ColorSpace::device_gray();
        let cmyk = ColorSpace::device_cmyk();
        let c = Color::gray(0.0);
        let result = convert_color(&c, &gray, &cmyk).unwrap();
        assert_eq!(result.components.len(), 4);
        assert!((result.components[3] - 1.0).abs() < 1e-6);
    }

    #[test]
    fn test_convert_rgb_to_gray() {
        let rgb = ColorSpace::device_rgb();
        let gray = ColorSpace::device_gray();
        let c = Color::rgb(1.0, 1.0, 1.0);
        let result = convert_color(&c, &rgb, &gray).unwrap();
        assert_eq!(result.components.len(), 1);
        assert!((result.components[0] - 1.0).abs() < 1e-6);
    }

    #[test]
    fn test_convert_rgb_to_cmyk() {
        let rgb = ColorSpace::device_rgb();
        let cmyk = ColorSpace::device_cmyk();
        let c = Color::rgb(1.0, 0.0, 0.0);
        let result = convert_color(&c, &rgb, &cmyk).unwrap();
        assert_eq!(result.components.len(), 4);
        assert!(result.components[0] < 0.01);
        assert!(result.components[1] > 0.99);
    }

    #[test]
    fn test_convert_cmyk_to_rgb() {
        let cmyk = ColorSpace::device_cmyk();
        let rgb = ColorSpace::device_rgb();
        let c = Color::cmyk(0.0, 0.0, 0.0, 0.0);
        let result = convert_color(&c, &cmyk, &rgb).unwrap();
        assert_eq!(result.components.len(), 3);
        assert!((result.components[0] - 1.0).abs() < 1e-6);
        assert!((result.components[1] - 1.0).abs() < 1e-6);
        assert!((result.components[2] - 1.0).abs() < 1e-6);
    }

    #[test]
    fn test_convert_cmyk_to_gray() {
        let cmyk = ColorSpace::device_cmyk();
        let gray = ColorSpace::device_gray();
        let c = Color::cmyk(0.0, 0.0, 0.0, 1.0);
        let result = convert_color(&c, &cmyk, &gray).unwrap();
        assert_eq!(result.components.len(), 1);
        assert!(result.components[0] < 0.01);
    }

    #[test]
    fn test_convert_calgray_gamma() {
        let calgray = ColorSpace {
            cs_type: ColorSpaceType::CalGray {
                white_point: [1.0, 1.0, 1.0],
                black_point: [0.0, 0.0, 0.0],
                gamma: 2.0,
            },
        };
        let rgb = ColorSpace::device_rgb();
        let c = Color::gray(0.25);
        let result = convert_color(&c, &calgray, &rgb).unwrap();
        let expected = 0.25_f64.powf(1.0 / 2.0);
        assert!((result.components[0] - expected).abs() < 1e-5);
    }

    #[test]
    fn test_convert_calrgb_identity_matrix() {
        let calrgb = ColorSpace {
            cs_type: ColorSpaceType::CalRGB {
                white_point: [1.0, 1.0, 1.0],
                black_point: [0.0, 0.0, 0.0],
                gamma: [1.0, 1.0, 1.0],
                matrix: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
            },
        };
        let rgb = ColorSpace::device_rgb();
        let c = Color::rgb(0.5, 0.5, 0.5);
        let result = convert_color(&c, &calrgb, &rgb).unwrap();
        assert!(result.components[0] > 0.7 && result.components[0] < 0.75);
        assert!(result.components[1] > 0.7 && result.components[1] < 0.75);
        assert!(result.components[2] > 0.7 && result.components[2] < 0.75);
    }

    #[test]
    fn test_convert_lab_to_rgb() {
        let lab = ColorSpace {
            cs_type: ColorSpaceType::Lab {
                white_point: [0.95, 1.0, 1.09],
                black_point: [0.0, 0.0, 0.0],
                range: [-100.0, 100.0, -100.0, 100.0],
            },
        };
        let rgb = ColorSpace::device_rgb();
        let c = Color::new(&[100.0, 0.0, 0.0]);
        let result = convert_color(&c, &lab, &rgb).unwrap();
        assert_eq!(result.components.len(), 3);
        assert!(result.components.iter().all(|&v| v >= 0.0 && v <= 1.0));
        assert!(result.components[0] > 0.9);
        assert!(result.components[1] > 0.9);
        assert!(result.components[2] > 0.8);
        let c_mid = Color::new(&[50.0, 0.0, 0.0]);
        let result_mid = convert_color(&c_mid, &lab, &rgb).unwrap();
        assert!(result_mid.components[0] > 0.2 && result_mid.components[0] < 0.8);
    }

    #[test]
    fn test_convert_indexed_lookup() {
        let mut lookup = vec![0u8; 3 * 4];
        lookup[0] = 255;
        lookup[1] = 0;
        lookup[2] = 0;
        let indexed = ColorSpace {
            cs_type: ColorSpaceType::Indexed {
                base: Box::new(ColorSpace::device_rgb()),
                hival: 3,
                lookup,
            },
        };
        let rgb = ColorSpace::device_rgb();
        let c = Color::new(&[0.0]);
        let result = convert_color(&c, &indexed, &rgb).unwrap();
        assert!((result.components[0] - 1.0).abs() < 1e-6);
        assert!((result.components[1] - 0.0).abs() < 1e-6);
        assert!((result.components[2] - 0.0).abs() < 1e-6);
    }

    #[test]
    fn test_convert_black() {
        let gray = ColorSpace::device_gray();
        let rgb = ColorSpace::device_rgb();
        let c = Color::gray(0.0);
        let result = convert_color(&c, &gray, &rgb).unwrap();
        assert!((result.components[0] - 0.0).abs() < 1e-10);
        assert!((result.components[1] - 0.0).abs() < 1e-10);
        assert!((result.components[2] - 0.0).abs() < 1e-10);
    }

    #[test]
    fn test_convert_white() {
        let gray = ColorSpace::device_gray();
        let cmyk = ColorSpace::device_cmyk();
        let c = Color::gray(1.0);
        let result = convert_color(&c, &gray, &cmyk).unwrap();
        assert!((result.components[0] - 0.0).abs() < 1e-10);
        assert!((result.components[1] - 0.0).abs() < 1e-10);
        assert!((result.components[2] - 0.0).abs() < 1e-10);
        assert!((result.components[3] - 0.0).abs() < 1e-10);
    }

    #[test]
    fn test_convert_same_space() {
        let rgb = ColorSpace::device_rgb();
        let c = Color::rgb(0.3, 0.6, 0.9);
        let result = convert_color(&c, &rgb, &rgb).unwrap();
        assert_eq!(result.components.len(), 3);
        assert!((result.components[0] - 0.3).abs() < 1e-10);
    }

    #[test]
    fn test_convert_pattern_error() {
        let pattern = ColorSpace {
            cs_type: ColorSpaceType::Pattern,
        };
        let rgb = ColorSpace::device_rgb();
        let c = Color::gray(0.5);
        let result = convert_color(&c, &pattern, &rgb);
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_ref_error() {
        let obj = Object::Ref(ObjRef::new(1, 0));
        let result = ColorSpace::from_pdf_object(&obj);
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_unknown_name_error() {
        let obj = Object::Name(Name::new("UnknownSpace"));
        let result = ColorSpace::from_pdf_object(&obj);
        assert!(result.is_err());
    }
}
