//! PDF pattern and shading support per PDF spec
//!
//! Implements tiling patterns, shading patterns (axial, radial, mesh stubs),
//! and PDF function evaluation.

use crate::fitz::error::{Error, Result};
use crate::pdf::colorspace::{Color, ColorSpace};
use crate::pdf::object::{Dict, Name, Object};

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

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)),
    }
}

fn parse_bbox(dict: &Dict) -> [f64; 4] {
    dict.get(&Name::new("BBox"))
        .and_then(|o| o.as_array())
        .filter(|a| a.len() >= 4)
        .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),
                a.get(3).and_then(get_number).unwrap_or(0.0),
            ]
        })
        .unwrap_or([0.0, 0.0, 0.0, 0.0])
}

fn parse_matrix(dict: &Dict) -> [f64; 6] {
    dict.get(&Name::new("Matrix"))
        .and_then(|o| o.as_array())
        .filter(|a| a.len() >= 6)
        .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),
            ]
        })
        .unwrap_or([1.0, 0.0, 0.0, 0.0, 1.0, 0.0])
}

// ============================================================================
// PatternType
// ============================================================================

/// PDF pattern type: tiling or shading
#[derive(Debug, Clone)]
pub enum PatternType {
    Tiling(TilingPattern),
    Shading(ShadingPattern),
}

// ============================================================================
// TilingPattern
// ============================================================================

/// Paint type for tiling patterns (PDF 1.4 spec)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaintType {
    /// Colored - pattern defines its own colors
    Colored = 1,
    /// Uncolored - pattern uses current fill/stroke color
    Uncolored = 2,
}

/// Tiling type for spacing (PDF 1.4 spec)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TilingType {
    /// Constant spacing - cells spaced by XStep, YStep
    ConstantSpacing = 1,
    /// No distortion - scale to fit, preserve aspect
    NoDistortion = 2,
    /// Faster - allow distortion for speed
    Faster = 3,
}

/// Tiling pattern dictionary and content
#[derive(Debug, Clone)]
pub struct TilingPattern {
    pub paint_type: PaintType,
    pub tiling_type: TilingType,
    pub bbox: [f64; 4],
    pub x_step: f64,
    pub y_step: f64,
    pub matrix: [f64; 6],
    pub content_stream: Vec<u8>,
}

impl TilingPattern {
    /// Parse a tiling pattern from a pattern dictionary and optional stream.
    /// The dict may be from a Stream object or a Dict; Resources must be resolved.
    pub fn parse(dict: &Dict, content_stream: Vec<u8>) -> Result<Self> {
        let pattern_type = dict
            .get(&Name::new("PatternType"))
            .and_then(|o| o.as_int())
            .unwrap_or(1);
        if pattern_type != 1 {
            return Err(Error::pdf(format!(
                "Expected PatternType 1 (tiling), got {}",
                pattern_type
            )));
        }

        let paint_type = dict
            .get(&Name::new("PaintType"))
            .and_then(|o| o.as_int())
            .unwrap_or(1);
        let paint_type = match paint_type {
            1 => PaintType::Colored,
            2 => PaintType::Uncolored,
            n => return Err(Error::pdf(format!("Invalid PaintType: {}", n))),
        };

        let tiling_type = dict
            .get(&Name::new("TilingType"))
            .and_then(|o| o.as_int())
            .unwrap_or(1);
        let tiling_type = match tiling_type {
            1 => TilingType::ConstantSpacing,
            2 => TilingType::NoDistortion,
            3 => TilingType::Faster,
            n => return Err(Error::pdf(format!("Invalid TilingType: {}", n))),
        };

        let bbox = parse_bbox(dict);

        let x_step = dict
            .get(&Name::new("XStep"))
            .and_then(get_number)
            .filter(|&x| x > 0.0)
            .unwrap_or(1.0);
        let y_step = dict
            .get(&Name::new("YStep"))
            .and_then(get_number)
            .filter(|&y| y > 0.0)
            .unwrap_or(1.0);

        let matrix = parse_matrix(dict);

        Ok(Self {
            paint_type,
            tiling_type,
            bbox,
            x_step,
            y_step,
            matrix,
            content_stream,
        })
    }
}

/// Parse a tiling pattern from a PDF stream object (dict + data).
/// Resolves the stream's dict and content.
pub fn parse_tiling_pattern(stream: &Object) -> Result<TilingPattern> {
    let (dict, data) = match stream {
        Object::Stream { dict, data } => (dict, data.clone()),
        Object::Ref(_) => {
            return Err(Error::pdf("Pattern stream must be resolved before parsing"));
        }
        _ => return Err(Error::pdf("Tiling pattern must be a stream object")),
    };

    TilingPattern::parse(dict, data)
}

// ============================================================================
// ShadingType
// ============================================================================

/// Shading type per PDF spec
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShadingType {
    FunctionBased = 1,
    Axial = 2,
    Radial = 3,
    FreeFormGouraud = 4,
    LatticeFormGouraud = 5,
    CoonsPatch = 6,
    TensorProductPatch = 7,
}

// ============================================================================
// ShadingPattern
// ============================================================================

/// Shading pattern - references a shading dictionary
#[derive(Debug, Clone)]
pub struct ShadingPattern {
    pub shading_type: ShadingType,
    pub colorspace: ColorSpace,
    pub bbox: Option<[f64; 4]>,
    pub anti_alias: bool,
    pub matrix: [f64; 6],
    pub shading: Shading,
}

// ============================================================================
// Shading
// ============================================================================

/// Shading dictionary content - axial, radial, or mesh (stub)
#[derive(Debug, Clone)]
pub enum Shading {
    Axial(AxialShading),
    Radial(RadialShading),
    /// Mesh shadings (4-7): structure parsed, sampling not implemented
    MeshStub {
        shading_type: ShadingType,
    },
}

/// Axial shading: linear gradient along axis (x0,y0) -> (x1,y1)
#[derive(Debug, Clone)]
pub struct AxialShading {
    pub coords: [f64; 4], // x0, y0, x1, y1
    pub domain: [f64; 2],
    pub function: PdfFunction,
    pub extend: (bool, bool), // extend start, extend end
}

/// Radial shading: circular gradient between two circles
#[derive(Debug, Clone)]
pub struct RadialShading {
    pub coords: [f64; 6], // x0, y0, r0, x1, y1, r1
    pub domain: [f64; 2],
    pub function: PdfFunction,
    pub extend: (bool, bool),
}

// ============================================================================
// PdfFunction
// ============================================================================

/// PDF function types
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FunctionType {
    Sampled = 0,
    Exponential = 2,
    Stitching = 3,
    PostScript = 4,
}

/// PDF function - Type 2 (exponential) fully implemented, others stubbed
#[derive(Debug, Clone)]
pub enum PdfFunction {
    /// Type 2: f(x) = C0 + (C1 - C0) * x^n
    Exponential {
        domain: [f64; 2],
        range: Option<[f64; 2]>,
        n: f64,
        c0: Vec<f64>,
        c1: Vec<f64>,
    },
    /// Type 0, 3, 4: structure only, evaluation returns stub
    Stub { function_type: FunctionType },
}

impl PdfFunction {
    /// Evaluate the function for given input values.
    /// For Type 2: single input x, multiple outputs.
    /// For stubs: returns zeros.
    pub fn evaluate(&self, inputs: &[f64]) -> Result<Vec<f64>> {
        match self {
            PdfFunction::Exponential {
                domain,
                range,
                n,
                c0,
                c1,
            } => {
                let x = inputs.get(0).copied().unwrap_or(0.0);
                let x = x.clamp(domain[0], domain[1]);
                let t = if (domain[1] - domain[0]).abs() < 1e-15 {
                    1.0
                } else {
                    (x - domain[0]) / (domain[1] - domain[0])
                };
                let t = if t <= 0.0 {
                    0.0
                } else if t >= 1.0 {
                    1.0
                } else {
                    t.powf(*n)
                };

                let mut out = Vec::with_capacity(c0.len().max(c1.len()));
                for i in 0..c0.len().max(c1.len()) {
                    let c0_i = c0.get(i).copied().unwrap_or(0.0);
                    let c1_i = c1.get(i).copied().unwrap_or(1.0);
                    let v = c0_i + (c1_i - c0_i) * t;
                    out.push(v);
                }

                if let Some(r) = range {
                    for v in &mut out {
                        *v = v.clamp(r[0], r[1]);
                    }
                }

                Ok(out)
            }
            PdfFunction::Stub { .. } => Ok(vec![0.0]),
        }
    }
}

/// Parse a PDF function from a function dictionary or stream.
/// Indirect refs must be resolved before calling.
pub fn parse_pdf_function(obj: &Object) -> Result<PdfFunction> {
    let dict = match obj {
        Object::Stream { dict, .. } => dict,
        Object::Dict(d) => d,
        Object::Ref(_) => {
            return Err(Error::pdf("Function must be resolved before parsing"));
        }
        _ => return Err(Error::pdf("Function must be dict or stream")),
    };

    let func_type = dict
        .get(&Name::new("FunctionType"))
        .and_then(|o| o.as_int())
        .unwrap_or(0);

    match func_type {
        2 => {
            let domain = dict
                .get(&Name::new("Domain"))
                .and_then(|o| o.as_array())
                .filter(|a| a.len() >= 2)
                .map(|a| {
                    [
                        a.get(0).and_then(get_number).unwrap_or(0.0),
                        a.get(1).and_then(get_number).unwrap_or(1.0),
                    ]
                })
                .unwrap_or([0.0, 1.0]);

            let range = dict
                .get(&Name::new("Range"))
                .and_then(|o| o.as_array())
                .filter(|a| a.len() >= 2)
                .map(|a| {
                    [
                        a.get(0).and_then(get_number).unwrap_or(0.0),
                        a.get(1).and_then(get_number).unwrap_or(1.0),
                    ]
                });

            let n = dict
                .get(&Name::new("N"))
                .and_then(get_number)
                .filter(|&n| n.is_finite())
                .ok_or_else(|| Error::pdf("Exponential function requires /N"))?;

            let c0 = dict
                .get(&Name::new("C0"))
                .and_then(|o| o.as_array())
                .map(|a| a.iter().filter_map(get_number).collect())
                .unwrap_or_else(|| vec![0.0]);

            let c1 = dict
                .get(&Name::new("C1"))
                .and_then(|o| o.as_array())
                .map(|a| a.iter().filter_map(get_number).collect())
                .unwrap_or_else(|| vec![1.0]);

            Ok(PdfFunction::Exponential {
                domain,
                range,
                n,
                c0,
                c1,
            })
        }
        0 | 3 | 4 => Ok(PdfFunction::Stub {
            function_type: match func_type {
                0 => FunctionType::Sampled,
                3 => FunctionType::Stitching,
                4 => FunctionType::PostScript,
                _ => FunctionType::Sampled,
            },
        }),
        n => Err(Error::pdf(format!("Unsupported function type: {}", n))),
    }
}

/// Evaluate a PDF function for given input values
pub fn evaluate_function(func: &PdfFunction, inputs: &[f64]) -> Result<Vec<f64>> {
    func.evaluate(inputs)
}

// ============================================================================
// Shading parsing
// ============================================================================

/// Parse a shading dictionary. Supports Type 2 (Axial) and Type 3 (Radial) fully.
/// Types 4-7 parse structure but do not implement sampling.
pub fn parse_shading(obj: &Object) -> Result<Shading> {
    let dict = match obj {
        Object::Dict(d) => d,
        Object::Stream { dict, .. } => dict,
        Object::Ref(_) => {
            return Err(Error::pdf("Shading must be resolved before parsing"));
        }
        _ => return Err(Error::pdf("Shading must be dict or stream")),
    };

    let shading_type_val = dict
        .get(&Name::new("ShadingType"))
        .and_then(|o| o.as_int())
        .ok_or_else(|| Error::pdf("Shading dictionary missing ShadingType"))?;

    let shading_type = match shading_type_val {
        1 => ShadingType::FunctionBased,
        2 => ShadingType::Axial,
        3 => ShadingType::Radial,
        4 => ShadingType::FreeFormGouraud,
        5 => ShadingType::LatticeFormGouraud,
        6 => ShadingType::CoonsPatch,
        7 => ShadingType::TensorProductPatch,
        n => return Err(Error::pdf(format!("Invalid ShadingType: {}", n))),
    };

    let domain = dict
        .get(&Name::new("Domain"))
        .and_then(|o| o.as_array())
        .filter(|a| a.len() >= 2)
        .map(|a| {
            [
                a.get(0).and_then(get_number).unwrap_or(0.0),
                a.get(1).and_then(get_number).unwrap_or(1.0),
            ]
        })
        .unwrap_or([0.0, 1.0]);

    let extend_arr = dict
        .get(&Name::new("Extend"))
        .and_then(|o| o.as_array())
        .filter(|a| a.len() >= 2);
    let extend = extend_arr
        .map(|a| {
            (
                a.get(0).and_then(|o| o.as_bool()).unwrap_or(false),
                a.get(1).and_then(|o| o.as_bool()).unwrap_or(false),
            )
        })
        .unwrap_or((false, false));

    let function_obj = dict
        .get(&Name::new("Function"))
        .ok_or_else(|| Error::pdf("Shading requires Function"))?;

    let function = parse_pdf_function(function_obj)?;

    match shading_type {
        ShadingType::Axial => {
            let coords_arr = dict
                .get(&Name::new("Coords"))
                .and_then(|o| o.as_array())
                .filter(|a| a.len() >= 4)
                .ok_or_else(|| Error::pdf("Axial shading requires Coords [x0 y0 x1 y1]"))?;

            let coords = [
                coords_arr.get(0).and_then(get_number).unwrap_or(0.0),
                coords_arr.get(1).and_then(get_number).unwrap_or(0.0),
                coords_arr.get(2).and_then(get_number).unwrap_or(1.0),
                coords_arr.get(3).and_then(get_number).unwrap_or(0.0),
            ];

            Ok(Shading::Axial(AxialShading {
                coords,
                domain,
                function,
                extend,
            }))
        }
        ShadingType::Radial => {
            let coords_arr = dict
                .get(&Name::new("Coords"))
                .and_then(|o| o.as_array())
                .filter(|a| a.len() >= 6)
                .ok_or_else(|| Error::pdf("Radial shading requires Coords [x0 y0 r0 x1 y1 r1]"))?;

            let coords = [
                coords_arr.get(0).and_then(get_number).unwrap_or(0.0),
                coords_arr.get(1).and_then(get_number).unwrap_or(0.0),
                coords_arr.get(2).and_then(get_number).unwrap_or(0.0),
                coords_arr.get(3).and_then(get_number).unwrap_or(1.0),
                coords_arr.get(4).and_then(get_number).unwrap_or(0.0),
                coords_arr.get(5).and_then(get_number).unwrap_or(1.0),
            ];

            Ok(Shading::Radial(RadialShading {
                coords,
                domain,
                function,
                extend,
            }))
        }
        ShadingType::FunctionBased
        | ShadingType::FreeFormGouraud
        | ShadingType::LatticeFormGouraud
        | ShadingType::CoonsPatch
        | ShadingType::TensorProductPatch => Ok(Shading::MeshStub { shading_type }),
    }
}

/// Parse a shading pattern from a pattern dictionary.
/// The Shading entry must be resolved before calling.
pub fn parse_shading_pattern(dict: &Dict, shading_obj: &Object) -> Result<ShadingPattern> {
    let shading = parse_shading(shading_obj)?;

    let shading_type = match &shading {
        Shading::Axial(_) => ShadingType::Axial,
        Shading::Radial(_) => ShadingType::Radial,
        Shading::MeshStub { shading_type } => *shading_type,
    };

    let colorspace_obj = if let Object::Dict(d) = shading_obj {
        d.get(&Name::new("ColorSpace"))
    } else if let Object::Stream { dict: d, .. } = shading_obj {
        d.get(&Name::new("ColorSpace"))
    } else {
        None
    };

    let colorspace = colorspace_obj
        .map(|o| ColorSpace::from_pdf_object(o))
        .transpose()?
        .unwrap_or_else(ColorSpace::device_rgb);

    let bbox = dict
        .get(&Name::new("BBox"))
        .and_then(|o| o.as_array())
        .filter(|a| a.len() >= 4)
        .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),
                a.get(3).and_then(get_number).unwrap_or(0.0),
            ]
        });

    let anti_alias = dict
        .get(&Name::new("AntiAlias"))
        .and_then(|o| o.as_bool())
        .unwrap_or(false);

    let matrix = parse_matrix(dict);

    Ok(ShadingPattern {
        shading_type,
        colorspace,
        bbox,
        anti_alias,
        matrix,
        shading,
    })
}

// ============================================================================
// Shading evaluation
// ============================================================================

/// Evaluate axial shading at point (x, y).
/// t = (P - C0) · (C1 - C0) / |C1 - C0|², clamped to domain, then extended.
pub fn evaluate_axial_shading(shading: &AxialShading, x: f64, y: f64) -> Result<Color> {
    let [x0, y0, x1, y1] = shading.coords;
    let dx = x1 - x0;
    let dy = y1 - y0;
    let len_sq = dx * dx + dy * dy;

    let t = if len_sq < 1e-20 {
        0.5
    } else {
        let px = x - x0;
        let py = y - y0;
        ((px * dx + py * dy) / len_sq).clamp(0.0, 1.0)
    };

    let t_domain = shading.domain[0] + t * (shading.domain[1] - shading.domain[0]);
    let t_final = if t < 0.0 && shading.extend.0 {
        shading.domain[0]
    } else if t > 1.0 && shading.extend.1 {
        shading.domain[1]
    } else {
        t_domain.clamp(shading.domain[0], shading.domain[1])
    };

    let outputs = shading.function.evaluate(&[t_final])?;
    Ok(Color::new(&outputs))
}

/// Evaluate radial shading at point (x, y).
/// For concentric circles: t = (r - r0) / (r1 - r0) where r = distance from (x,y) to (x0,y0).
/// For non-concentric, uses distance to start circle center as approximation.
pub fn evaluate_radial_shading(shading: &RadialShading, x: f64, y: f64) -> Result<Color> {
    let [x0, y0, r0, x1, y1, r1] = shading.coords;

    let dx = x - x0;
    let dy = y - y0;
    let r = (dx * dx + dy * dy).sqrt();

    let denom = r1 - r0;

    let t = if denom.abs() < 1e-20 {
        0.5
    } else {
        ((r - r0) / denom).clamp(0.0, 1.0)
    };

    let t_domain = shading.domain[0] + t * (shading.domain[1] - shading.domain[0]);
    let t_final = if t < 0.0 && shading.extend.0 {
        shading.domain[0]
    } else if t > 1.0 && shading.extend.1 {
        shading.domain[1]
    } else {
        t_domain.clamp(shading.domain[0], shading.domain[1])
    };

    let outputs = shading.function.evaluate(&[t_final])?;
    Ok(Color::new(&outputs))
}

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

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

    #[test]
    fn test_parse_tiling_pattern() {
        let mut dict = Dict::new();
        dict.insert(Name::new("Type"), Object::Name(Name::new("Pattern")));
        dict.insert(Name::new("PatternType"), Object::Int(1));
        dict.insert(Name::new("PaintType"), Object::Int(1));
        dict.insert(Name::new("TilingType"), Object::Int(1));
        dict.insert(
            Name::new("BBox"),
            Object::Array(vec![
                Object::Real(0.0),
                Object::Real(0.0),
                Object::Real(10.0),
                Object::Real(10.0),
            ]),
        );
        dict.insert(Name::new("XStep"), Object::Real(10.0));
        dict.insert(Name::new("YStep"), Object::Real(10.0));

        let stream = Object::Stream {
            dict: dict.clone(),
            data: b"0.5 0.5 0.5 rg 0 0 10 10 re f".to_vec(),
        };

        let pattern = parse_tiling_pattern(&stream).unwrap();
        assert_eq!(pattern.paint_type, PaintType::Colored);
        assert_eq!(pattern.tiling_type, TilingType::ConstantSpacing);
        assert_eq!(pattern.bbox, [0.0, 0.0, 10.0, 10.0]);
        assert!((pattern.x_step - 10.0).abs() < 1e-10);
        assert!((pattern.y_step - 10.0).abs() < 1e-10);
        assert_eq!(pattern.content_stream, b"0.5 0.5 0.5 rg 0 0 10 10 re f");
    }

    #[test]
    fn test_parse_tiling_pattern_uncolored() {
        let mut dict = Dict::new();
        dict.insert(Name::new("PatternType"), Object::Int(1));
        dict.insert(Name::new("PaintType"), Object::Int(2));
        dict.insert(Name::new("TilingType"), Object::Int(2));
        dict.insert(
            Name::new("BBox"),
            Object::Array(vec![
                Object::Int(0),
                Object::Int(0),
                Object::Int(5),
                Object::Int(5),
            ]),
        );

        let pattern = TilingPattern::parse(&dict, vec![]).unwrap();
        assert_eq!(pattern.paint_type, PaintType::Uncolored);
        assert_eq!(pattern.tiling_type, TilingType::NoDistortion);
    }

    #[test]
    fn test_parse_pdf_function_exponential() {
        let dict = make_dict(&[
            ("FunctionType", Object::Int(2)),
            (
                "Domain",
                Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
            ),
            ("N", Object::Real(1.0)),
            (
                "C0",
                Object::Array(vec![
                    Object::Real(0.0),
                    Object::Real(0.0),
                    Object::Real(0.0),
                ]),
            ),
            (
                "C1",
                Object::Array(vec![
                    Object::Real(1.0),
                    Object::Real(1.0),
                    Object::Real(1.0),
                ]),
            ),
        ]);

        let func = parse_pdf_function(&Object::Dict(dict)).unwrap();
        let out0 = evaluate_function(&func, &[0.0]).unwrap();
        let out1 = evaluate_function(&func, &[1.0]).unwrap();
        let out_half = evaluate_function(&func, &[0.5]).unwrap();

        assert_eq!(out0.len(), 3);
        assert!((out0[0] - 0.0).abs() < 1e-10);
        assert!((out1[0] - 1.0).abs() < 1e-10);
        assert!((out_half[0] - 0.5).abs() < 1e-10);
    }

    #[test]
    fn test_evaluate_function_exponential_n2() {
        let dict = make_dict(&[
            ("FunctionType", Object::Int(2)),
            (
                "Domain",
                Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
            ),
            ("N", Object::Real(2.0)),
            ("C0", Object::Array(vec![Object::Real(0.0)])),
            ("C1", Object::Array(vec![Object::Real(1.0)])),
        ]);

        let func = parse_pdf_function(&Object::Dict(dict)).unwrap();
        let out_half = evaluate_function(&func, &[0.5]).unwrap();
        assert_eq!(out_half.len(), 1);
        assert!((out_half[0] - 0.25).abs() < 1e-10);
    }

    #[test]
    fn test_parse_axial_shading() {
        let mut shading_dict = Dict::new();
        shading_dict.insert(Name::new("ShadingType"), Object::Int(2));
        shading_dict.insert(
            Name::new("ColorSpace"),
            Object::Name(Name::new("DeviceRGB")),
        );
        shading_dict.insert(
            Name::new("Coords"),
            Object::Array(vec![
                Object::Real(0.0),
                Object::Real(0.0),
                Object::Real(100.0),
                Object::Real(0.0),
            ]),
        );
        shading_dict.insert(
            Name::new("Function"),
            Object::Dict(make_dict(&[
                ("FunctionType", Object::Int(2)),
                (
                    "Domain",
                    Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
                ),
                ("N", Object::Real(1.0)),
                (
                    "C0",
                    Object::Array(vec![
                        Object::Real(1.0),
                        Object::Real(0.0),
                        Object::Real(0.0),
                    ]),
                ),
                (
                    "C1",
                    Object::Array(vec![
                        Object::Real(0.0),
                        Object::Real(0.0),
                        Object::Real(1.0),
                    ]),
                ),
            ])),
        );

        let shading = parse_shading(&Object::Dict(shading_dict)).unwrap();
        if let Shading::Axial(axial) = shading {
            assert_eq!(axial.coords, [0.0, 0.0, 100.0, 0.0]);
            let color_start = evaluate_axial_shading(&axial, 0.0, 0.0).unwrap();
            let color_end = evaluate_axial_shading(&axial, 100.0, 0.0).unwrap();
            assert!((color_start.components[0] - 1.0).abs() < 1e-6);
            assert!((color_end.components[2] - 1.0).abs() < 1e-6);
        } else {
            panic!("Expected Axial shading");
        }
    }

    #[test]
    fn test_evaluate_axial_shading_midpoint() {
        let mut shading_dict = Dict::new();
        shading_dict.insert(Name::new("ShadingType"), Object::Int(2));
        shading_dict.insert(
            Name::new("ColorSpace"),
            Object::Name(Name::new("DeviceRGB")),
        );
        shading_dict.insert(
            Name::new("Coords"),
            Object::Array(vec![
                Object::Real(0.0),
                Object::Real(0.0),
                Object::Real(10.0),
                Object::Real(0.0),
            ]),
        );
        shading_dict.insert(
            Name::new("Function"),
            Object::Dict(make_dict(&[
                ("FunctionType", Object::Int(2)),
                (
                    "Domain",
                    Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
                ),
                ("N", Object::Real(1.0)),
                (
                    "C0",
                    Object::Array(vec![
                        Object::Real(0.0),
                        Object::Real(0.0),
                        Object::Real(0.0),
                    ]),
                ),
                (
                    "C1",
                    Object::Array(vec![
                        Object::Real(1.0),
                        Object::Real(1.0),
                        Object::Real(1.0),
                    ]),
                ),
            ])),
        );

        let shading = parse_shading(&Object::Dict(shading_dict)).unwrap();
        if let Shading::Axial(axial) = shading {
            let color = evaluate_axial_shading(&axial, 5.0, 0.0).unwrap();
            assert!((color.components[0] - 0.5).abs() < 1e-5);
        } else {
            panic!("Expected Axial shading");
        }
    }

    #[test]
    fn test_parse_radial_shading() {
        let mut shading_dict = Dict::new();
        shading_dict.insert(Name::new("ShadingType"), Object::Int(3));
        shading_dict.insert(
            Name::new("ColorSpace"),
            Object::Name(Name::new("DeviceRGB")),
        );
        shading_dict.insert(
            Name::new("Coords"),
            Object::Array(vec![
                Object::Real(50.0),
                Object::Real(50.0),
                Object::Real(0.0),
                Object::Real(50.0),
                Object::Real(50.0),
                Object::Real(50.0),
            ]),
        );
        shading_dict.insert(
            Name::new("Function"),
            Object::Dict(make_dict(&[
                ("FunctionType", Object::Int(2)),
                (
                    "Domain",
                    Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
                ),
                ("N", Object::Real(1.0)),
                (
                    "C0",
                    Object::Array(vec![
                        Object::Real(1.0),
                        Object::Real(1.0),
                        Object::Real(1.0),
                    ]),
                ),
                (
                    "C1",
                    Object::Array(vec![
                        Object::Real(0.0),
                        Object::Real(0.0),
                        Object::Real(0.0),
                    ]),
                ),
            ])),
        );

        let shading = parse_shading(&Object::Dict(shading_dict)).unwrap();
        if let Shading::Radial(radial) = shading {
            assert_eq!(radial.coords[0], 50.0);
            assert_eq!(radial.coords[2], 0.0);
            assert_eq!(radial.coords[5], 50.0);

            let color_center = evaluate_radial_shading(&radial, 50.0, 50.0).unwrap();
            let color_edge = evaluate_radial_shading(&radial, 100.0, 50.0).unwrap();
            assert!(color_center.components[0] > 0.9);
            assert!(color_edge.components[0] < 0.2);
        } else {
            panic!("Expected Radial shading");
        }
    }

    #[test]
    fn test_evaluate_radial_shading() {
        let mut shading_dict = Dict::new();
        shading_dict.insert(Name::new("ShadingType"), Object::Int(3));
        shading_dict.insert(
            Name::new("ColorSpace"),
            Object::Name(Name::new("DeviceGray")),
        );
        shading_dict.insert(
            Name::new("Coords"),
            Object::Array(vec![
                Object::Real(0.0),
                Object::Real(0.0),
                Object::Real(0.0),
                Object::Real(0.0),
                Object::Real(0.0),
                Object::Real(10.0),
            ]),
        );
        shading_dict.insert(
            Name::new("Function"),
            Object::Dict(make_dict(&[
                ("FunctionType", Object::Int(2)),
                (
                    "Domain",
                    Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
                ),
                ("N", Object::Real(1.0)),
                ("C0", Object::Array(vec![Object::Real(0.0)])),
                ("C1", Object::Array(vec![Object::Real(1.0)])),
            ])),
        );

        let shading = parse_shading(&Object::Dict(shading_dict)).unwrap();
        if let Shading::Radial(radial) = shading {
            let color_center = evaluate_radial_shading(&radial, 0.0, 0.0).unwrap();
            let color_at_5 = evaluate_radial_shading(&radial, 5.0, 0.0).unwrap();
            assert!(color_center.components[0] < 0.1);
            assert!((color_at_5.components[0] - 0.5).abs() < 0.1);
        } else {
            panic!("Expected Radial shading");
        }
    }

    #[test]
    fn test_function_stub() {
        let dict = make_dict(&[("FunctionType", Object::Int(0))]);
        let func = parse_pdf_function(&Object::Dict(dict)).unwrap();
        let out = evaluate_function(&func, &[0.5]).unwrap();
        assert_eq!(out, vec![0.0]);
    }
}
