//! PDF Resource FFI Module
//!
//! Provides PDF resource management including fonts, images, colorspaces,
//! patterns, shadings, functions, and XObjects.

use crate::ffi::colorspace::{
    COLORSPACES, Colorspace, ColorspaceType, FZ_COLORSPACE_CMYK, FZ_COLORSPACE_GRAY,
    FZ_COLORSPACE_LAB, FZ_COLORSPACE_RGB,
};
use crate::ffi::image::IMAGES;
use crate::ffi::pdf_object::types::{PDF_OBJECTS, PdfObj, PdfObjType};
use crate::ffi::shade::{SHADES, Shade, ShadeType};
use crate::ffi::{BUFFERS, Handle, HandleStore};
use crate::fitz::image::Image;
use std::collections::HashMap;
use std::ffi::{CStr, c_char, c_void};
use std::ptr;
use std::sync::LazyLock;

// ============================================================================
// Type Aliases
// ============================================================================

type ContextHandle = Handle;
type DocumentHandle = Handle;
type PdfObjHandle = Handle;
type FontHandle = Handle;
type ImageHandle = Handle;
type ColorspaceHandle = Handle;
type ShadeHandle = Handle;
type StreamHandle = Handle;
type BufferHandle = Handle;

// ============================================================================
// Font Resource Constants
// ============================================================================

/// Simple font resource type
pub const PDF_SIMPLE_FONT_RESOURCE: i32 = 1;
/// CID font resource type
pub const PDF_CID_FONT_RESOURCE: i32 = 2;
/// CJK font resource type
pub const PDF_CJK_FONT_RESOURCE: i32 = 3;

/// Latin encoding
pub const PDF_SIMPLE_ENCODING_LATIN: i32 = 0;
/// Greek encoding
pub const PDF_SIMPLE_ENCODING_GREEK: i32 = 1;
/// Cyrillic encoding
pub const PDF_SIMPLE_ENCODING_CYRILLIC: i32 = 2;

// ============================================================================
// Resource Key Structures
// ============================================================================

/// Font resource key for lookup/caching
#[derive(Debug, Clone, Default)]
#[repr(C)]
pub struct FontResourceKey {
    /// MD5 digest of font data
    pub digest: [u8; 16],
    /// Font type (simple, CID, CJK)
    pub font_type: i32,
    /// Encoding type
    pub encoding: i32,
    /// Local xref flag
    pub local_xref: i32,
}

/// Colorspace resource key for lookup/caching
#[derive(Debug, Clone, Default)]
#[repr(C)]
pub struct ColorspaceResourceKey {
    /// MD5 digest of colorspace data
    pub digest: [u8; 16],
    /// Local xref flag
    pub local_xref: i32,
}

// ============================================================================
// Resource Stack
// ============================================================================

/// PDF Resource stack for hierarchical resource lookup
#[derive(Debug, Clone)]
pub struct ResourceStack {
    /// Resources dictionary handle
    pub resources: PdfObjHandle,
    /// Next stack entry (parent)
    pub next: Option<Handle>,
}

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

impl ResourceStack {
    pub fn new() -> Self {
        Self {
            resources: 0,
            next: None,
        }
    }

    pub fn with_resources(resources: PdfObjHandle) -> Self {
        Self {
            resources,
            next: None,
        }
    }
}

// ============================================================================
// Pattern Structure
// ============================================================================

/// PDF Pattern
#[derive(Debug, Clone)]
pub struct Pattern {
    /// Reference count
    pub refs: i32,
    /// Is mask pattern
    pub is_mask: bool,
    /// X step
    pub xstep: f32,
    /// Y step
    pub ystep: f32,
    /// Pattern matrix [a, b, c, d, e, f]
    pub matrix: [f32; 6],
    /// Bounding box [x0, y0, x1, y1]
    pub bbox: [f32; 4],
    /// Document handle
    pub document: DocumentHandle,
    /// Resources dictionary handle
    pub resources: PdfObjHandle,
    /// Contents stream handle
    pub contents: PdfObjHandle,
    /// Unique ID for caching
    pub id: i32,
}

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

impl Pattern {
    pub fn new() -> Self {
        static PATTERN_ID: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(1);
        Self {
            refs: 1,
            is_mask: false,
            xstep: 0.0,
            ystep: 0.0,
            matrix: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
            bbox: [0.0, 0.0, 0.0, 0.0],
            document: 0,
            resources: 0,
            contents: 0,
            id: PATTERN_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst),
        }
    }
}

// ============================================================================
// Function Structure
// ============================================================================

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

/// PDF Function
#[derive(Debug, Clone)]
pub struct Function {
    /// Reference count
    pub refs: i32,
    /// Function type
    pub func_type: FunctionType,
    /// Number of input values
    pub n_inputs: i32,
    /// Number of output values
    pub n_outputs: i32,
    /// Domain [min0, max0, min1, max1, ...]
    pub domain: Vec<f32>,
    /// Range [min0, max0, min1, max1, ...]
    pub range: Vec<f32>,
    /// Sample data (for Type 0)
    pub samples: Vec<f32>,
    /// C0 values (for Type 2)
    pub c0: Vec<f32>,
    /// C1 values (for Type 2)
    pub c1: Vec<f32>,
    /// Exponent (for Type 2)
    pub n: f32,
    /// Sub-functions (for Type 3)
    pub funcs: Vec<Handle>,
    /// Bounds (for Type 3)
    pub bounds: Vec<f32>,
    /// Encode values (for Type 3)
    pub encode: Vec<f32>,
}

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

impl Function {
    pub fn new() -> Self {
        Self {
            refs: 1,
            func_type: FunctionType::Sampled,
            n_inputs: 1,
            n_outputs: 1,
            domain: vec![0.0, 1.0],
            range: vec![0.0, 1.0],
            samples: Vec::new(),
            c0: vec![0.0],
            c1: vec![1.0],
            n: 1.0,
            funcs: Vec::new(),
            bounds: Vec::new(),
            encode: Vec::new(),
        }
    }

    /// Evaluate the function, dispatching to the type-specific method.
    pub fn eval(&self, input: &[f32], output: &mut [f32]) {
        self.run(input, output);
    }

    /// Internal evaluation entry point -- separate name so that
    /// sub-function calls from eval_stitching can call through without
    /// going through the same public API (avoids confusion with the FFI
    /// `pdf_eval_function` wrapper).
    fn run(&self, input: &[f32], output: &mut [f32]) {
        match self.func_type {
            FunctionType::Sampled => self.eval_sampled(input, output),
            FunctionType::Exponential => self.eval_exponential(input, output),
            FunctionType::Stitching => self.eval_stitching(input, output),
            FunctionType::PostScript => self.eval_postscript(input, output),
        }
    }

    fn eval_sampled(&self, input: &[f32], output: &mut [f32]) {
        // Simple linear interpolation for sampled function
        if self.samples.is_empty() || output.is_empty() {
            return;
        }
        let t = input.first().copied().unwrap_or(0.0).clamp(0.0, 1.0);
        let idx = (t * (self.samples.len() - 1) as f32) as usize;
        for (i, out) in output.iter_mut().enumerate() {
            *out = self.samples.get(idx + i).copied().unwrap_or(0.0);
        }
    }

    fn eval_exponential(&self, input: &[f32], output: &mut [f32]) {
        // f(x) = C0 + x^N * (C1 - C0)
        let x = input.first().copied().unwrap_or(0.0).clamp(0.0, 1.0);
        let x_n = x.powf(self.n);
        for (i, out) in output.iter_mut().enumerate() {
            let c0 = self.c0.get(i).copied().unwrap_or(0.0);
            let c1 = self.c1.get(i).copied().unwrap_or(1.0);
            *out = c0 + x_n * (c1 - c0);
        }
    }

    fn eval_stitching(&self, input: &[f32], output: &mut [f32]) {
        if self.funcs.is_empty() || output.is_empty() {
            return;
        }

        let x = input.first().copied().unwrap_or(0.0);
        let k = self.funcs.len();

        // Determine which sub-function interval x falls into.
        // Domain: [domain[0] .. bounds[0] .. bounds[1] .. ... .. domain[1]]
        // Sub-function i covers the interval [low_bound, high_bound).
        let domain_lo = self.domain.first().copied().unwrap_or(0.0);
        let domain_hi = self.domain.get(1).copied().unwrap_or(1.0);
        let x_clamped = x.clamp(domain_lo, domain_hi);

        let mut func_idx = k - 1; // default to last
        for (i, &bound) in self.bounds.iter().enumerate() {
            if x_clamped < bound {
                func_idx = i;
                break;
            }
        }
        func_idx = func_idx.min(k - 1);

        // Determine the sub-interval boundaries
        let low_bound = if func_idx == 0 {
            domain_lo
        } else {
            self.bounds.get(func_idx - 1).copied().unwrap_or(domain_lo)
        };
        let high_bound = if func_idx < self.bounds.len() {
            self.bounds[func_idx]
        } else {
            domain_hi
        };

        // Map x from [low_bound, high_bound] to the encode range for
        // this sub-function: [encode[2*i], encode[2*i+1]]
        let encode_lo = self.encode.get(func_idx * 2).copied().unwrap_or(0.0);
        let encode_hi = self.encode.get(func_idx * 2 + 1).copied().unwrap_or(1.0);

        let mapped = if (high_bound - low_bound).abs() < f32::EPSILON {
            encode_lo
        } else {
            encode_lo + (x_clamped - low_bound) / (high_bound - low_bound) * (encode_hi - encode_lo)
        };

        // Run the selected sub-function
        let sub_handle = self.funcs[func_idx];
        if let Some(sub_arc) = FUNCTIONS.get(sub_handle) {
            let sub = sub_arc.lock().unwrap();
            sub.run(&[mapped], output);
        } else {
            // Fallback: linear pass-through clamped to [0, 1]
            for out in output.iter_mut() {
                *out = mapped.clamp(0.0, 1.0);
            }
        }
    }

    fn eval_postscript(&self, input: &[f32], output: &mut [f32]) {
        // PostScript Type 4 calculator function.
        // Implements a stack-based calculator supporting the arithmetic,
        // relational, and conditional operators defined in PDF spec
        // section 7.10.5 (Type 4 functions).
        //
        // The calculator pushes input values, executes the stored
        // program (self.samples repurposed as an encoded instruction
        // stream), and pops output values.  When no program is stored
        // we fall back to identity: output[i] = input[i].

        if output.is_empty() {
            return;
        }

        // If no program data is stored, use identity mapping
        if self.samples.is_empty() {
            for (i, out) in output.iter_mut().enumerate() {
                *out = input.get(i).copied().unwrap_or(0.0);
            }
            // Clamp outputs to range
            for (i, out) in output.iter_mut().enumerate() {
                let lo = self.range.get(i * 2).copied().unwrap_or(0.0);
                let hi = self.range.get(i * 2 + 1).copied().unwrap_or(1.0);
                *out = out.clamp(lo, hi);
            }
            return;
        }

        // Execute the PostScript program stored in self.samples.
        // Each f32 encodes an opcode via its integer value.
        let mut stack: Vec<f32> = Vec::with_capacity(32);

        // Push inputs onto the stack
        for &v in input {
            stack.push(v);
        }

        for &raw in &self.samples {
            let opcode = raw as i32;
            match opcode {
                // 1 = add
                1 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap();
                        let a = stack.pop().unwrap();
                        stack.push(a + b);
                    }
                }
                // 2 = sub
                2 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap();
                        let a = stack.pop().unwrap();
                        stack.push(a - b);
                    }
                }
                // 3 = mul
                3 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap();
                        let a = stack.pop().unwrap();
                        stack.push(a * b);
                    }
                }
                // 4 = div
                4 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap();
                        let a = stack.pop().unwrap();
                        if b.abs() > f32::EPSILON {
                            stack.push(a / b);
                        } else {
                            stack.push(0.0);
                        }
                    }
                }
                // 5 = neg
                5 => {
                    if let Some(v) = stack.pop() {
                        stack.push(-v);
                    }
                }
                // 6 = abs
                6 => {
                    if let Some(v) = stack.pop() {
                        stack.push(v.abs());
                    }
                }
                // 7 = floor
                7 => {
                    if let Some(v) = stack.pop() {
                        stack.push(v.floor());
                    }
                }
                // 8 = ceiling
                8 => {
                    if let Some(v) = stack.pop() {
                        stack.push(v.ceil());
                    }
                }
                // 9 = round
                9 => {
                    if let Some(v) = stack.pop() {
                        stack.push(v.round());
                    }
                }
                // 10 = truncate
                10 => {
                    if let Some(v) = stack.pop() {
                        stack.push(v.trunc());
                    }
                }
                // 11 = sqrt
                11 => {
                    if let Some(v) = stack.pop() {
                        stack.push(if v >= 0.0 { v.sqrt() } else { 0.0 });
                    }
                }
                // 12 = mod
                12 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap();
                        let a = stack.pop().unwrap();
                        if b.abs() > f32::EPSILON {
                            stack.push(a % b);
                        } else {
                            stack.push(0.0);
                        }
                    }
                }
                // 13 = exp (a^b)
                13 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap();
                        let a = stack.pop().unwrap();
                        stack.push(a.powf(b));
                    }
                }
                // 14 = sin (degrees)
                14 => {
                    if let Some(v) = stack.pop() {
                        stack.push(v.to_radians().sin());
                    }
                }
                // 15 = cos (degrees)
                15 => {
                    if let Some(v) = stack.pop() {
                        stack.push(v.to_radians().cos());
                    }
                }
                // 16 = atan (result in degrees)
                16 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap();
                        let a = stack.pop().unwrap();
                        stack.push(a.atan2(b).to_degrees());
                    }
                }
                // 17 = log (base 10)
                17 => {
                    if let Some(v) = stack.pop() {
                        stack.push(if v > 0.0 { v.log10() } else { 0.0 });
                    }
                }
                // 18 = ln (natural log)
                18 => {
                    if let Some(v) = stack.pop() {
                        stack.push(if v > 0.0 { v.ln() } else { 0.0 });
                    }
                }
                // 19 = dup
                19 => {
                    if let Some(&v) = stack.last() {
                        stack.push(v);
                    }
                }
                // 20 = exch (swap top two)
                20 => {
                    let len = stack.len();
                    if len >= 2 {
                        stack.swap(len - 1, len - 2);
                    }
                }
                // 21 = pop
                21 => {
                    stack.pop();
                }
                // 22 = copy (duplicate top n items)
                22 => {
                    if let Some(v) = stack.pop() {
                        let n = v as usize;
                        let len = stack.len();
                        if n > 0 && n <= len {
                            let start = len - n;
                            let items: Vec<f32> = stack[start..].to_vec();
                            stack.extend_from_slice(&items);
                        }
                    }
                }
                // 23 = index (copy nth item to top)
                23 => {
                    if let Some(v) = stack.pop() {
                        let n = v as usize;
                        let len = stack.len();
                        if n < len {
                            let val = stack[len - 1 - n];
                            stack.push(val);
                        }
                    }
                }
                // 24 = roll (roll n items j positions)
                24 => {
                    if stack.len() >= 2 {
                        let j = stack.pop().unwrap() as i32;
                        let n = stack.pop().unwrap() as usize;
                        let len = stack.len();
                        if n > 0 && n <= len {
                            let start = len - n;
                            let sub = &mut stack[start..];
                            let shift = ((j % n as i32) + n as i32) as usize % n;
                            sub.rotate_right(shift);
                        }
                    }
                }
                // 30 = eq
                30 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap();
                        let a = stack.pop().unwrap();
                        stack.push(if (a - b).abs() < f32::EPSILON {
                            1.0
                        } else {
                            0.0
                        });
                    }
                }
                // 31 = ne
                31 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap();
                        let a = stack.pop().unwrap();
                        stack.push(if (a - b).abs() >= f32::EPSILON {
                            1.0
                        } else {
                            0.0
                        });
                    }
                }
                // 32 = gt
                32 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap();
                        let a = stack.pop().unwrap();
                        stack.push(if a > b { 1.0 } else { 0.0 });
                    }
                }
                // 33 = ge
                33 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap();
                        let a = stack.pop().unwrap();
                        stack.push(if a >= b { 1.0 } else { 0.0 });
                    }
                }
                // 34 = lt
                34 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap();
                        let a = stack.pop().unwrap();
                        stack.push(if a < b { 1.0 } else { 0.0 });
                    }
                }
                // 35 = le
                35 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap();
                        let a = stack.pop().unwrap();
                        stack.push(if a <= b { 1.0 } else { 0.0 });
                    }
                }
                // 40 = and (bitwise on integer casts)
                40 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap() as i32;
                        let a = stack.pop().unwrap() as i32;
                        stack.push((a & b) as f32);
                    }
                }
                // 41 = or
                41 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap() as i32;
                        let a = stack.pop().unwrap() as i32;
                        stack.push((a | b) as f32);
                    }
                }
                // 42 = xor
                42 => {
                    if stack.len() >= 2 {
                        let b = stack.pop().unwrap() as i32;
                        let a = stack.pop().unwrap() as i32;
                        stack.push((a ^ b) as f32);
                    }
                }
                // 43 = not (bitwise)
                43 => {
                    if let Some(v) = stack.pop() {
                        let i = v as i32;
                        stack.push((!i) as f32);
                    }
                }
                // 44 = bitshift (positive = left, negative = right)
                44 => {
                    if stack.len() >= 2 {
                        let shift = stack.pop().unwrap() as i32;
                        let val = stack.pop().unwrap() as i32;
                        let result = if shift >= 0 {
                            val.wrapping_shl(shift as u32)
                        } else {
                            val.wrapping_shr((-shift) as u32)
                        };
                        stack.push(result as f32);
                    }
                }
                // Default: treat the raw value as a literal to push
                _ => {
                    stack.push(raw);
                }
            }
        }

        // Pop output values from the stack (last pushed = output[n-1])
        let n_out = output.len();
        for i in (0..n_out).rev() {
            output[i] = stack.pop().unwrap_or(0.0);
        }

        // Clamp outputs to the declared range
        for (i, out) in output.iter_mut().enumerate() {
            let lo = self.range.get(i * 2).copied().unwrap_or(0.0);
            let hi = self.range.get(i * 2 + 1).copied().unwrap_or(1.0);
            *out = out.clamp(lo, hi);
        }
    }

    pub fn size(&self) -> usize {
        std::mem::size_of::<Function>()
            + self.domain.len() * 4
            + self.range.len() * 4
            + self.samples.len() * 4
            + self.c0.len() * 4
            + self.c1.len() * 4
            + self.funcs.len() * 8
            + self.bounds.len() * 4
            + self.encode.len() * 4
    }
}

// ============================================================================
// Resource Tables
// ============================================================================

/// Document resource tables for deduplication
#[derive(Debug, Default)]
pub struct ResourceTables {
    /// Font resources by digest
    pub fonts: HashMap<[u8; 16], PdfObjHandle>,
    /// Colorspace resources by digest
    pub colorspaces: HashMap<[u8; 16], PdfObjHandle>,
    /// Image resources by digest
    pub images: HashMap<[u8; 16], PdfObjHandle>,
    /// Pattern resources by digest
    pub patterns: HashMap<[u8; 16], PdfObjHandle>,
    /// Shading resources by digest
    pub shadings: HashMap<[u8; 16], PdfObjHandle>,
}

impl ResourceTables {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn clear(&mut self) {
        self.fonts.clear();
        self.colorspaces.clear();
        self.images.clear();
        self.patterns.clear();
        self.shadings.clear();
    }
}

// ============================================================================
// Global Handle Stores
// ============================================================================

pub static RESOURCE_STACKS: LazyLock<HandleStore<ResourceStack>> = LazyLock::new(HandleStore::new);
pub static PATTERNS: LazyLock<HandleStore<Pattern>> = LazyLock::new(HandleStore::new);
pub static FUNCTIONS: LazyLock<HandleStore<Function>> = LazyLock::new(HandleStore::new);
pub static DOC_RESOURCE_TABLES: LazyLock<
    std::sync::Mutex<HashMap<DocumentHandle, ResourceTables>>,
> = LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));

/// Global item store mapping PDF object handles to opaque pointers.
/// This provides the MuPDF-compatible pdf_store_item / pdf_find_item /
/// pdf_remove_item API: callers associate a PDF object key with an
/// arbitrary cached value (image pixmap, font metrics, etc.).
///
/// The store does NOT own the pointed-to memory; it merely records the
/// association so that callers can avoid recomputing expensive results.
static PDF_ITEM_STORE: LazyLock<std::sync::Mutex<HashMap<PdfObjHandle, usize>>> =
    LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));

// ============================================================================
// FFI Functions - Store Operations
// ============================================================================

/// Store an item in the PDF store, keyed by a PDF object handle.
///
/// The caller retains ownership of `val`; the store only records the
/// pointer so that future `pdf_find_item` calls can retrieve it.
/// `itemsize` is advisory and used for cache-pressure heuristics in
/// MuPDF -- we record the association regardless of size.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_store_item(
    _ctx: ContextHandle,
    key: PdfObjHandle,
    val: *mut c_void,
    _itemsize: usize,
) {
    if key == 0 || val.is_null() {
        return;
    }
    if let Ok(mut store) = PDF_ITEM_STORE.lock() {
        store.insert(key, val as usize);
    }
}

/// Find a previously stored item by its PDF object key.
///
/// Returns the pointer that was passed to `pdf_store_item`, or null if
/// the key has no associated item.  `drop` identifies the type of value
/// (used by MuPDF's type-dispatched store); we accept any type.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_find_item(
    _ctx: ContextHandle,
    _drop: *const c_void,
    key: PdfObjHandle,
) -> *mut c_void {
    if key == 0 {
        return ptr::null_mut();
    }
    if let Ok(store) = PDF_ITEM_STORE.lock() {
        if let Some(&addr) = store.get(&key) {
            return addr as *mut c_void;
        }
    }
    ptr::null_mut()
}

/// Remove an item from the PDF store by its PDF object key.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_remove_item(_ctx: ContextHandle, _drop: *const c_void, key: PdfObjHandle) {
    if key == 0 {
        return;
    }
    if let Ok(mut store) = PDF_ITEM_STORE.lock() {
        store.remove(&key);
    }
}

/// Empty the document's store.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_empty_store(_ctx: ContextHandle, doc: DocumentHandle) {
    let mut tables = DOC_RESOURCE_TABLES.lock().unwrap();
    if let Some(t) = tables.get_mut(&doc) {
        t.clear();
    }
}

/// Purge locals from the store.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_purge_locals_from_store(_ctx: ContextHandle, doc: DocumentHandle) {
    let mut tables = DOC_RESOURCE_TABLES.lock().unwrap();
    if let Some(t) = tables.get_mut(&doc) {
        t.clear();
    }
}

/// Purge a specific object number from the document's resource tables
/// and from the global item store.
///
/// Iterates every resource table (fonts, colorspaces, images, patterns,
/// shadings) and removes entries whose cached PDF object handle matches
/// `num`.  Also removes the handle from the global PDF_ITEM_STORE.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_purge_object_from_store(_ctx: ContextHandle, doc: DocumentHandle, num: i32) {
    let handle_to_purge = num as u64;

    // Remove from document resource tables
    if let Ok(mut tables) = DOC_RESOURCE_TABLES.lock() {
        if let Some(t) = tables.get_mut(&doc) {
            t.fonts.retain(|_, &mut v| v != handle_to_purge);
            t.colorspaces.retain(|_, &mut v| v != handle_to_purge);
            t.images.retain(|_, &mut v| v != handle_to_purge);
            t.patterns.retain(|_, &mut v| v != handle_to_purge);
            t.shadings.retain(|_, &mut v| v != handle_to_purge);
        }
    }

    // Remove from global item store
    if let Ok(mut store) = PDF_ITEM_STORE.lock() {
        store.remove(&handle_to_purge);
    }
}

// ============================================================================
// FFI Functions - Font Resources
// ============================================================================

/// Find a font resource by digest.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_find_font_resource(
    _ctx: ContextHandle,
    doc: DocumentHandle,
    _font_type: i32,
    _encoding: i32,
    _item: FontHandle,
    key: *mut FontResourceKey,
) -> PdfObjHandle {
    if key.is_null() {
        return 0;
    }

    let tables = DOC_RESOURCE_TABLES.lock().unwrap();
    if let Some(t) = tables.get(&doc) {
        let digest = unsafe { (*key).digest };
        if let Some(&obj) = t.fonts.get(&digest) {
            return obj;
        }
    }
    0
}

/// Insert a font resource.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_insert_font_resource(
    _ctx: ContextHandle,
    doc: DocumentHandle,
    key: *const FontResourceKey,
    obj: PdfObjHandle,
) -> PdfObjHandle {
    if key.is_null() {
        return 0;
    }

    let mut tables = DOC_RESOURCE_TABLES.lock().unwrap();
    let t = tables.entry(doc).or_default();
    let digest = unsafe { (*key).digest };
    t.fonts.insert(digest, obj);
    obj
}

// ============================================================================
// FFI Functions - Colorspace Resources
// ============================================================================

/// Find a colorspace resource by digest.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_find_colorspace_resource(
    _ctx: ContextHandle,
    doc: DocumentHandle,
    _item: ColorspaceHandle,
    key: *mut ColorspaceResourceKey,
) -> PdfObjHandle {
    if key.is_null() {
        return 0;
    }

    let tables = DOC_RESOURCE_TABLES.lock().unwrap();
    if let Some(t) = tables.get(&doc) {
        let digest = unsafe { (*key).digest };
        if let Some(&obj) = t.colorspaces.get(&digest) {
            return obj;
        }
    }
    0
}

/// Insert a colorspace resource.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_insert_colorspace_resource(
    _ctx: ContextHandle,
    doc: DocumentHandle,
    key: *const ColorspaceResourceKey,
    obj: PdfObjHandle,
) -> PdfObjHandle {
    if key.is_null() {
        return 0;
    }

    let mut tables = DOC_RESOURCE_TABLES.lock().unwrap();
    let t = tables.entry(doc).or_default();
    let digest = unsafe { (*key).digest };
    t.colorspaces.insert(digest, obj);
    obj
}

/// Drop resource tables for a document.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_drop_resource_tables(_ctx: ContextHandle, doc: DocumentHandle) {
    let mut tables = DOC_RESOURCE_TABLES.lock().unwrap();
    tables.remove(&doc);
}

/// Purge local resources.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_purge_local_resources(_ctx: ContextHandle, doc: DocumentHandle) {
    let mut tables = DOC_RESOURCE_TABLES.lock().unwrap();
    if let Some(t) = tables.get_mut(&doc) {
        t.clear();
    }
}

// ============================================================================
// FFI Functions - Resource Stack
// ============================================================================

/// Create a new resource stack.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_resource_stack(_ctx: ContextHandle, resources: PdfObjHandle) -> Handle {
    let stack = ResourceStack::with_resources(resources);
    RESOURCE_STACKS.insert(stack)
}

/// Push a resource stack entry.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_push_resource_stack(
    _ctx: ContextHandle,
    stack: Handle,
    resources: PdfObjHandle,
) -> Handle {
    let mut new_stack = ResourceStack::with_resources(resources);
    new_stack.next = Some(stack);
    RESOURCE_STACKS.insert(new_stack)
}

/// Pop a resource stack entry.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_pop_resource_stack(_ctx: ContextHandle, stack: Handle) -> Handle {
    if let Some(stack_arc) = RESOURCE_STACKS.get(stack) {
        let s = stack_arc.lock().unwrap();
        if let Some(next) = s.next {
            RESOURCE_STACKS.remove(stack);
            return next;
        }
    }
    0
}

/// Drop a resource stack.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_drop_resource_stack(_ctx: ContextHandle, stack: Handle) {
    RESOURCE_STACKS.remove(stack);
}

/// Lookup a named resource in the resource stack.
///
/// Walks the stack from the current (innermost) frame outward through
/// parent frames.  At each level the function reads the resources
/// dictionary handle, looks up the sub-dictionary for `res_type`
/// (e.g. "Font", "XObject"), and then searches that sub-dictionary
/// for `name`.  Returns the first matching PDF object handle, or 0
/// if the resource is not found anywhere in the stack.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_lookup_resource(
    _ctx: ContextHandle,
    stack: Handle,
    res_type: PdfObjHandle,
    name: *const c_char,
) -> PdfObjHandle {
    if name.is_null() || stack == 0 {
        return 0;
    }

    let name_str = unsafe { CStr::from_ptr(name) }
        .to_str()
        .unwrap_or("")
        .to_string();
    if name_str.is_empty() {
        return 0;
    }

    // Resolve the resource type name (e.g. "Font", "XObject", "ColorSpace")
    let type_name = if let Some(arc) = PDF_OBJECTS.get(res_type) {
        if let Ok(guard) = arc.lock() {
            match &guard.obj_type {
                PdfObjType::Name(s) => s.clone(),
                _ => return 0,
            }
        } else {
            return 0;
        }
    } else {
        return 0;
    };

    // Walk the resource stack from innermost to outermost
    let mut current = Some(stack);
    while let Some(handle) = current {
        if let Some(stack_arc) = RESOURCE_STACKS.get(handle) {
            let s = stack_arc.lock().unwrap();
            let res_dict = s.resources;
            let next = s.next;
            // s is dropped here after we copy what we need
            drop(s);

            // Look for the type sub-dictionary in the resources dict
            if res_dict != 0 {
                if let Some(res_arc) = PDF_OBJECTS.get(res_dict) {
                    if let Ok(res_guard) = res_arc.lock() {
                        if let PdfObjType::Dict(entries) = &res_guard.obj_type {
                            // Find the sub-dict for this resource type (e.g. "Font")
                            if let Some((_, sub_dict_obj)) =
                                entries.iter().find(|(k, _)| k == &type_name)
                            {
                                // The sub-dict value may be an inline dict
                                if let PdfObjType::Dict(sub_entries) = &sub_dict_obj.obj_type {
                                    if let Some((_, val)) =
                                        sub_entries.iter().find(|(k, _)| k == &name_str)
                                    {
                                        // Found: insert a clone into the global store
                                        // and return the handle
                                        let result_handle = PDF_OBJECTS.insert(val.clone());
                                        return result_handle;
                                    }
                                }
                            }
                        }
                    }
                }
            }
            current = next;
        } else {
            break;
        }
    }
    0
}

// ============================================================================
// FFI Functions - Functions
// ============================================================================

/// Load a PDF function.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_load_function(
    _ctx: ContextHandle,
    _ref: PdfObjHandle,
    n_in: i32,
    n_out: i32,
) -> Handle {
    let mut func = Function::new();
    func.n_inputs = n_in;
    func.n_outputs = n_out;
    func.domain = vec![0.0, 1.0];
    func.range = (0..n_out).flat_map(|_| vec![0.0, 1.0]).collect();
    FUNCTIONS.insert(func)
}

/// Keep a function.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_keep_function(_ctx: ContextHandle, func: Handle) -> Handle {
    if let Some(func_arc) = FUNCTIONS.get(func) {
        let mut f = func_arc.lock().unwrap();
        f.refs += 1;
    }
    func
}

/// Drop a function.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_drop_function(_ctx: ContextHandle, func: Handle) {
    if let Some(func_arc) = FUNCTIONS.get(func) {
        let should_remove = {
            let mut f = func_arc.lock().unwrap();
            f.refs -= 1;
            f.refs <= 0
        };
        if should_remove {
            FUNCTIONS.remove(func);
        }
    }
}

/// Get function size.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_function_size(_ctx: ContextHandle, func: Handle) -> usize {
    if let Some(func_arc) = FUNCTIONS.get(func) {
        let f = func_arc.lock().unwrap();
        return f.size();
    }
    0
}

/// Evaluate a function.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_eval_function(
    _ctx: ContextHandle,
    func: Handle,
    input: *const f32,
    inlen: i32,
    output: *mut f32,
    outlen: i32,
) {
    if input.is_null() || output.is_null() || inlen <= 0 || outlen <= 0 {
        return;
    }

    if let Some(func_arc) = FUNCTIONS.get(func) {
        let f = func_arc.lock().unwrap();
        let input_slice = unsafe { std::slice::from_raw_parts(input, inlen as usize) };
        let output_slice = unsafe { std::slice::from_raw_parts_mut(output, outlen as usize) };
        f.eval(input_slice, output_slice);
    }
}

// ============================================================================
// FFI Functions - Patterns
// ============================================================================

/// Load a pattern.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_load_pattern(
    _ctx: ContextHandle,
    doc: DocumentHandle,
    _obj: PdfObjHandle,
) -> Handle {
    let mut pattern = Pattern::new();
    pattern.document = doc;
    PATTERNS.insert(pattern)
}

/// Keep a pattern.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_keep_pattern(_ctx: ContextHandle, pat: Handle) -> Handle {
    if let Some(pat_arc) = PATTERNS.get(pat) {
        let mut p = pat_arc.lock().unwrap();
        p.refs += 1;
    }
    pat
}

/// Drop a pattern.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_drop_pattern(_ctx: ContextHandle, pat: Handle) {
    if let Some(pat_arc) = PATTERNS.get(pat) {
        let should_remove = {
            let mut p = pat_arc.lock().unwrap();
            p.refs -= 1;
            p.refs <= 0
        };
        if should_remove {
            PATTERNS.remove(pat);
        }
    }
}

/// Get pattern properties.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_pattern_is_mask(_ctx: ContextHandle, pat: Handle) -> i32 {
    if let Some(pat_arc) = PATTERNS.get(pat) {
        let p = pat_arc.lock().unwrap();
        return if p.is_mask { 1 } else { 0 };
    }
    0
}

#[unsafe(no_mangle)]
pub extern "C" fn pdf_pattern_xstep(_ctx: ContextHandle, pat: Handle) -> f32 {
    if let Some(pat_arc) = PATTERNS.get(pat) {
        let p = pat_arc.lock().unwrap();
        return p.xstep;
    }
    0.0
}

#[unsafe(no_mangle)]
pub extern "C" fn pdf_pattern_ystep(_ctx: ContextHandle, pat: Handle) -> f32 {
    if let Some(pat_arc) = PATTERNS.get(pat) {
        let p = pat_arc.lock().unwrap();
        return p.ystep;
    }
    0.0
}

// ============================================================================
// FFI Functions - Colorspace Loading
// ============================================================================

/// Load a colorspace from a PDF object.
///
/// Examines the PDF object to determine the colorspace type.  Supports:
/// - Name objects: "DeviceGray", "DeviceRGB", "DeviceCMYK", "Lab"
/// - Array objects: first element is the colorspace family name,
///   remaining elements are parameters (e.g. [/ICCBased stream],
///   [/Indexed base hival lookup]).
///
/// Returns a colorspace handle, or 0 if the object cannot be parsed.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_load_colorspace(_ctx: ContextHandle, obj: PdfObjHandle) -> ColorspaceHandle {
    if obj == 0 {
        return 0;
    }

    if let Some(arc) = PDF_OBJECTS.get(obj) {
        if let Ok(guard) = arc.lock() {
            return load_colorspace_from_obj(&guard);
        }
    }
    0
}

/// Resolve a PdfObj into a colorspace handle.
fn load_colorspace_from_obj(obj: &PdfObj) -> ColorspaceHandle {
    match &obj.obj_type {
        PdfObjType::Name(name) => match name.as_str() {
            "DeviceGray" | "G" => FZ_COLORSPACE_GRAY,
            "DeviceRGB" | "RGB" => FZ_COLORSPACE_RGB,
            "DeviceCMYK" | "CMYK" => FZ_COLORSPACE_CMYK,
            "Lab" => FZ_COLORSPACE_LAB,
            // CalGray behaves like DeviceGray for rendering purposes
            "CalGray" => FZ_COLORSPACE_GRAY,
            // CalRGB behaves like DeviceRGB for rendering purposes
            "CalRGB" => FZ_COLORSPACE_RGB,
            "Pattern" => 0, // Pattern is not a true colorspace
            _ => 0,
        },
        PdfObjType::Array(elems) => {
            if elems.is_empty() {
                return 0;
            }
            // First element is the colorspace family name
            if let PdfObjType::Name(family) = &elems[0].obj_type {
                match family.as_str() {
                    "DeviceGray" | "G" => FZ_COLORSPACE_GRAY,
                    "DeviceRGB" | "RGB" => FZ_COLORSPACE_RGB,
                    "DeviceCMYK" | "CMYK" => FZ_COLORSPACE_CMYK,
                    "Lab" => FZ_COLORSPACE_LAB,
                    "CalGray" => FZ_COLORSPACE_GRAY,
                    "CalRGB" => FZ_COLORSPACE_RGB,
                    "ICCBased" => {
                        // ICCBased: [/ICCBased stream]
                        // Determine component count from the stream dict's /N
                        let n = if elems.len() > 1 {
                            extract_int_from_dict(&elems[1], "N").unwrap_or(3)
                        } else {
                            3
                        };
                        match n {
                            1 => FZ_COLORSPACE_GRAY,
                            3 => FZ_COLORSPACE_RGB,
                            4 => FZ_COLORSPACE_CMYK,
                            _ => {
                                // Create a custom colorspace for unusual N
                                let cs = Colorspace {
                                    cs_type: ColorspaceType::Icc,
                                    n,
                                    name: format!("ICCBased({})", n),
                                    base_cs: FZ_COLORSPACE_RGB,
                                    lookup: Vec::new(),
                                    high: 0,
                                    colorants: Vec::new(),
                                    icc_data: Vec::new(),
                                };
                                COLORSPACES.insert(cs) + 100 // CUSTOM_CS_OFFSET
                            }
                        }
                    }
                    "Indexed" | "I" => {
                        // [/Indexed base hival lookup]
                        if elems.len() < 4 {
                            return 0;
                        }
                        let base = load_colorspace_from_obj(&elems[1]);
                        let hival = match &elems[2].obj_type {
                            PdfObjType::Int(i) => *i as i32,
                            _ => return 0,
                        };
                        let cs = Colorspace {
                            cs_type: ColorspaceType::Indexed,
                            n: 1,
                            name: format!("Indexed({})", hival),
                            base_cs: base,
                            lookup: Vec::new(),
                            high: hival,
                            colorants: Vec::new(),
                            icc_data: Vec::new(),
                        };
                        COLORSPACES.insert(cs) + 100
                    }
                    "Separation" => {
                        // [/Separation name alternateCS tintTransform]
                        let cs = Colorspace {
                            cs_type: ColorspaceType::Separation,
                            n: 1,
                            name: "Separation".to_string(),
                            base_cs: if elems.len() > 2 {
                                load_colorspace_from_obj(&elems[2])
                            } else {
                                FZ_COLORSPACE_CMYK
                            },
                            lookup: Vec::new(),
                            high: 0,
                            colorants: Vec::new(),
                            icc_data: Vec::new(),
                        };
                        COLORSPACES.insert(cs) + 100
                    }
                    "DeviceN" => {
                        // [/DeviceN names alternateCS tintTransform]
                        let n = if elems.len() > 1 {
                            match &elems[1].obj_type {
                                PdfObjType::Array(names) => names.len() as i32,
                                _ => 1,
                            }
                        } else {
                            1
                        };
                        let cs = Colorspace {
                            cs_type: ColorspaceType::Separation,
                            n,
                            name: format!("DeviceN({})", n),
                            base_cs: if elems.len() > 2 {
                                load_colorspace_from_obj(&elems[2])
                            } else {
                                FZ_COLORSPACE_CMYK
                            },
                            lookup: Vec::new(),
                            high: 0,
                            colorants: Vec::new(),
                            icc_data: Vec::new(),
                        };
                        COLORSPACES.insert(cs) + 100
                    }
                    _ => 0,
                }
            } else {
                0
            }
        }
        _ => 0,
    }
}

/// Helper: extract an integer value from a PDF dict object by key name.
fn extract_int_from_dict(obj: &PdfObj, key: &str) -> Option<i32> {
    if let PdfObjType::Dict(entries) = &obj.obj_type {
        for (k, v) in entries {
            if k == key {
                if let PdfObjType::Int(i) = &v.obj_type {
                    return Some(*i as i32);
                }
            }
        }
    }
    // Also check if this is a stream (dict + data)
    if let PdfObjType::Stream { dict, .. } = &obj.obj_type {
        return extract_int_from_dict(dict, key);
    }
    None
}

/// Helper: extract a float value from a PDF dict object by key name.
fn extract_real_from_dict(obj: &PdfObj, key: &str) -> Option<f32> {
    if let PdfObjType::Dict(entries) = &obj.obj_type {
        for (k, v) in entries {
            if k == key {
                match &v.obj_type {
                    PdfObjType::Real(f) => return Some(*f as f32),
                    PdfObjType::Int(i) => return Some(*i as f32),
                    _ => {}
                }
            }
        }
    }
    if let PdfObjType::Stream { dict, .. } = &obj.obj_type {
        return extract_real_from_dict(dict, key);
    }
    None
}

/// Helper: extract a name value from a PDF dict object by key name.
fn extract_name_from_dict(obj: &PdfObj, key: &str) -> Option<String> {
    if let PdfObjType::Dict(entries) = &obj.obj_type {
        for (k, v) in entries {
            if k == key {
                if let PdfObjType::Name(s) = &v.obj_type {
                    return Some(s.clone());
                }
            }
        }
    }
    if let PdfObjType::Stream { dict, .. } = &obj.obj_type {
        return extract_name_from_dict(dict, key);
    }
    None
}

/// Helper: extract a sub-object from a PDF dict by key name.
fn extract_obj_from_dict<'a>(obj: &'a PdfObj, key: &str) -> Option<&'a PdfObj> {
    if let PdfObjType::Dict(entries) = &obj.obj_type {
        for (k, v) in entries {
            if k == key {
                return Some(v);
            }
        }
    }
    if let PdfObjType::Stream { dict, .. } = &obj.obj_type {
        return extract_obj_from_dict(dict, key);
    }
    None
}

/// Helper: extract a bool value from a PDF dict object by key name.
fn extract_bool_from_dict(obj: &PdfObj, key: &str) -> Option<bool> {
    if let PdfObjType::Dict(entries) = &obj.obj_type {
        for (k, v) in entries {
            if k == key {
                match &v.obj_type {
                    PdfObjType::Bool(b) => return Some(*b),
                    PdfObjType::Int(i) => return Some(*i != 0),
                    _ => {}
                }
            }
        }
    }
    if let PdfObjType::Stream { dict, .. } = &obj.obj_type {
        return extract_bool_from_dict(dict, key);
    }
    None
}

/// Helper: extract a float array from a PDF dict by key name.
fn extract_float_array_from_dict(obj: &PdfObj, key: &str) -> Option<Vec<f32>> {
    let sub = extract_obj_from_dict(obj, key)?;
    if let PdfObjType::Array(elems) = &sub.obj_type {
        let vals: Vec<f32> = elems
            .iter()
            .filter_map(|e| match &e.obj_type {
                PdfObjType::Real(f) => Some(*f as f32),
                PdfObjType::Int(i) => Some(*i as f32),
                _ => None,
            })
            .collect();
        if vals.is_empty() { None } else { Some(vals) }
    } else {
        None
    }
}

/// Get the document's output intent colorspace.
///
/// Searches the document's resource tables for a previously cached
/// output intent colorspace.  If none has been cached, returns a
/// default sRGB (DeviceRGB) handle, since most PDF workflows that
/// specify an output intent target sRGB or a similar RGB profile.
///
/// A full PDF/A or PDF/X pipeline would parse the /OutputIntents
/// array in the document catalog and extract the ICC profile from
/// the /DestOutputProfile stream.  This implementation returns the
/// cached value if one was stored via the resource tables, otherwise
/// defaults to DeviceRGB.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_document_output_intent(
    _ctx: ContextHandle,
    doc: DocumentHandle,
) -> ColorspaceHandle {
    // Check if an output intent was previously cached in resource tables
    if let Ok(tables) = DOC_RESOURCE_TABLES.lock() {
        if let Some(t) = tables.get(&doc) {
            // Convention: output intent stored under a well-known digest
            let output_intent_digest = [0xFFu8; 16];
            if let Some(&cs) = t.colorspaces.get(&output_intent_digest) {
                return cs;
            }
        }
    }
    // Default: sRGB / DeviceRGB
    FZ_COLORSPACE_RGB
}

/// Check if the given colorspace is a tint (Separation or DeviceN)
/// colorspace.  Tint colorspaces map a single tint value through a
/// tint transform to produce color in an alternate colorspace.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_is_tint_colorspace(_ctx: ContextHandle, cs: ColorspaceHandle) -> i32 {
    if cs == 0 {
        return 0;
    }
    // Device colorspaces (handles 1-5) are never tint
    if cs <= 5 {
        return 0;
    }
    // Custom colorspaces: check if Separation type
    if cs >= 100 {
        let real_handle = cs - 100;
        if let Some(cs_arc) = COLORSPACES.get(real_handle) {
            if let Ok(guard) = cs_arc.lock() {
                return i32::from(guard.cs_type == ColorspaceType::Separation);
            }
        }
    }
    0
}

/// Guess the number of components for a colorspace described by a
/// PDF object.  This examines the object structure (name or array)
/// and returns the component count without fully loading the
/// colorspace.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_guess_colorspace_components(_ctx: ContextHandle, obj: PdfObjHandle) -> i32 {
    if obj == 0 {
        return 3; // Default to RGB
    }

    if let Some(arc) = PDF_OBJECTS.get(obj) {
        if let Ok(guard) = arc.lock() {
            match &guard.obj_type {
                PdfObjType::Name(name) => {
                    return match name.as_str() {
                        "DeviceGray" | "G" | "CalGray" => 1,
                        "DeviceRGB" | "RGB" | "CalRGB" => 3,
                        "DeviceCMYK" | "CMYK" => 4,
                        "Lab" => 3,
                        _ => 3,
                    };
                }
                PdfObjType::Array(elems) => {
                    if let Some(first) = elems.first() {
                        if let PdfObjType::Name(family) = &first.obj_type {
                            return match family.as_str() {
                                "DeviceGray" | "G" | "CalGray" => 1,
                                "DeviceRGB" | "RGB" | "CalRGB" => 3,
                                "DeviceCMYK" | "CMYK" => 4,
                                "Lab" => 3,
                                "ICCBased" => {
                                    if elems.len() > 1 {
                                        extract_int_from_dict(&elems[1], "N").unwrap_or(3)
                                    } else {
                                        3
                                    }
                                }
                                "Indexed" | "I" => 1,
                                "Separation" => 1,
                                "DeviceN" => {
                                    if elems.len() > 1 {
                                        match &elems[1].obj_type {
                                            PdfObjType::Array(names) => names.len() as i32,
                                            _ => 1,
                                        }
                                    } else {
                                        1
                                    }
                                }
                                _ => 3,
                            };
                        }
                    }
                }
                _ => {}
            }
        }
    }
    3 // Default to RGB
}

// ============================================================================
// FFI Functions - Shading Loading
// ============================================================================

/// Load a shading from a PDF object dictionary.
///
/// Reads the /ShadingType, /ColorSpace, /BBox, /Background, and
/// type-specific entries (e.g. /Coords, /Extend for axial/radial)
/// from the PDF dictionary and creates a corresponding Shade object
/// in the global SHADES store.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_load_shading(
    _ctx: ContextHandle,
    _doc: DocumentHandle,
    obj: PdfObjHandle,
) -> ShadeHandle {
    if obj == 0 {
        return 0;
    }

    let shade = if let Some(arc) = PDF_OBJECTS.get(obj) {
        if let Ok(guard) = arc.lock() {
            build_shade_from_obj(&guard)
        } else {
            None
        }
    } else {
        None
    };

    match shade {
        Some(s) => SHADES.insert(s),
        None => {
            // Even if we cannot parse the object, create a default shade
            // so that callers always get a valid handle
            SHADES.insert(Shade::default())
        }
    }
}

/// Build a Shade struct from a PDF object.
fn build_shade_from_obj(obj: &PdfObj) -> Option<Shade> {
    let shading_type = extract_int_from_dict(obj, "ShadingType").unwrap_or(0);

    let shade_type = match shading_type {
        1 => ShadeType::Function,
        2 => ShadeType::Linear,
        3 => ShadeType::Radial,
        4 => ShadeType::FreeFormTriangle,
        5 => ShadeType::LatticeTriangle,
        6 => ShadeType::CoonsPatch,
        7 => ShadeType::TensorPatch,
        _ => ShadeType::None,
    };

    let mut shade = Shade {
        shade_type,
        ..Default::default()
    };

    // Parse colorspace
    if let Some(cs_obj) = extract_obj_from_dict(obj, "ColorSpace") {
        shade.colorspace = load_colorspace_from_obj(cs_obj) as u64;
    }

    // Parse BBox
    if let Some(bbox) = extract_float_array_from_dict(obj, "BBox") {
        if bbox.len() >= 4 {
            shade.bbox = [bbox[0], bbox[1], bbox[2], bbox[3]];
        }
    }

    // Parse Background
    if let Some(bg) = extract_float_array_from_dict(obj, "Background") {
        let mut bg_arr = [0.0f32; 4];
        for (i, &v) in bg.iter().take(4).enumerate() {
            bg_arr[i] = v;
        }
        shade.background = Some(bg_arr);
    }

    // Parse type-specific data
    match shading_type {
        2 => {
            // Axial shading: /Coords [x0 y0 x1 y1]
            if let Some(coords) = extract_float_array_from_dict(obj, "Coords") {
                if coords.len() >= 4 {
                    shade.linear_start = crate::ffi::shade::ShadePoint {
                        x: coords[0],
                        y: coords[1],
                    };
                    shade.linear_end = crate::ffi::shade::ShadePoint {
                        x: coords[2],
                        y: coords[3],
                    };
                }
            }
            // /Extend [bool bool]
            if let Some(ext_obj) = extract_obj_from_dict(obj, "Extend") {
                if let PdfObjType::Array(ext) = &ext_obj.obj_type {
                    if ext.len() >= 2 {
                        shade.extend_start = matches!(&ext[0].obj_type, PdfObjType::Bool(true));
                        shade.extend_end = matches!(&ext[1].obj_type, PdfObjType::Bool(true));
                    }
                }
            }
        }
        3 => {
            // Radial shading: /Coords [x0 y0 r0 x1 y1 r1]
            if let Some(coords) = extract_float_array_from_dict(obj, "Coords") {
                if coords.len() >= 6 {
                    shade.radial_start = crate::ffi::shade::ShadePoint {
                        x: coords[0],
                        y: coords[1],
                    };
                    shade.radial_r0 = coords[2];
                    shade.radial_end = crate::ffi::shade::ShadePoint {
                        x: coords[3],
                        y: coords[4],
                    };
                    shade.radial_r1 = coords[5];
                }
            }
            if let Some(ext_obj) = extract_obj_from_dict(obj, "Extend") {
                if let PdfObjType::Array(ext) = &ext_obj.obj_type {
                    if ext.len() >= 2 {
                        shade.extend_start = matches!(&ext[0].obj_type, PdfObjType::Bool(true));
                        shade.extend_end = matches!(&ext[1].obj_type, PdfObjType::Bool(true));
                    }
                }
            }
        }
        1 => {
            // Function-based: /Domain, /Matrix
            if let Some(domain) = extract_float_array_from_dict(obj, "Domain") {
                if domain.len() >= 4 {
                    shade.domain = [domain[0], domain[1], domain[2], domain[3]];
                }
            }
            if let Some(matrix) = extract_float_array_from_dict(obj, "Matrix") {
                if matrix.len() >= 6 {
                    shade.matrix = [
                        matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5],
                    ];
                }
            }
        }
        4..=7 => {
            // Mesh shadings
            shade.bits_per_coord = extract_int_from_dict(obj, "BitsPerCoordinate").unwrap_or(8);
            shade.bits_per_comp = extract_int_from_dict(obj, "BitsPerComponent").unwrap_or(8);
            shade.bits_per_flag = extract_int_from_dict(obj, "BitsPerFlag").unwrap_or(2);
        }
        _ => {}
    }

    Some(shade)
}

/// Sample shade function(s) across a parameter range.
///
/// Evaluates the given function handle(s) at `n` evenly-spaced
/// parameter values between `t0` and `t1` inclusive, and writes
/// the results into the `samples` array.  When multiple functions
/// are provided (`funcs` > 1), the results are interleaved: for
/// each sample point, all function outputs are written consecutively.
///
/// The caller must ensure `samples` has room for `n * funcs` floats.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_sample_shade_function(
    _ctx: ContextHandle,
    samples: *mut f32,
    n: i32,
    funcs: i32,
    func_handles: *const Handle,
    t0: f32,
    t1: f32,
) {
    if samples.is_null() || n <= 0 || funcs <= 0 || func_handles.is_null() {
        return;
    }

    let n_samples = n as usize;
    let n_funcs = funcs as usize;
    let handles = unsafe { std::slice::from_raw_parts(func_handles, n_funcs) };
    let out_slice = unsafe { std::slice::from_raw_parts_mut(samples, n_samples * n_funcs) };

    for i in 0..n_samples {
        let t = if n_samples > 1 {
            t0 + (t1 - t0) * (i as f32) / ((n_samples - 1) as f32)
        } else {
            t0
        };

        let input = [t];
        for (j, &fh) in handles.iter().enumerate() {
            let idx = i * n_funcs + j;
            if let Some(func_arc) = FUNCTIONS.get(fh) {
                let f = func_arc.lock().unwrap();
                let mut out = [0.0f32; 1];
                f.run(&input, &mut out);
                out_slice[idx] = out[0];
            } else {
                // Fallback: use the parameter value directly
                out_slice[idx] = t;
            }
        }
    }
}

// ============================================================================
// FFI Functions - Image Loading
// ============================================================================

/// Load an image from a PDF image XObject dictionary.
///
/// Reads /Width, /Height, /BitsPerComponent, and /ColorSpace from
/// the dictionary and creates an Image in the global IMAGES store.
/// The actual pixel data would normally be decompressed from the
/// stream; here we create an image with the correct dimensions that
/// callers can render into.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_load_image(
    _ctx: ContextHandle,
    _doc: DocumentHandle,
    obj: PdfObjHandle,
) -> ImageHandle {
    if obj == 0 {
        return 0;
    }

    let (w, h) = if let Some(arc) = PDF_OBJECTS.get(obj) {
        if let Ok(guard) = arc.lock() {
            let width = extract_int_from_dict(&guard, "Width")
                .or_else(|| extract_int_from_dict(&guard, "W"))
                .unwrap_or(1);
            let height = extract_int_from_dict(&guard, "Height")
                .or_else(|| extract_int_from_dict(&guard, "H"))
                .unwrap_or(1);
            (width.max(1), height.max(1))
        } else {
            (1, 1)
        }
    } else {
        (1, 1)
    };

    let image = Image::new(w, h, None);
    IMAGES.insert(image)
}

/// Load an inline image from a content stream.
///
/// Inline images embed their dictionary and data directly in the
/// content stream.  This reads /W (width), /H (height), and /BPC
/// (bits per component) from the inline dict object and creates
/// a corresponding Image.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_load_inline_image(
    _ctx: ContextHandle,
    _doc: DocumentHandle,
    _rdb: Handle,
    dict: PdfObjHandle,
    _file: StreamHandle,
) -> ImageHandle {
    if dict == 0 {
        return 0;
    }

    let (w, h) = if let Some(arc) = PDF_OBJECTS.get(dict) {
        if let Ok(guard) = arc.lock() {
            // Inline images use abbreviated keys: W, H, BPC
            let width = extract_int_from_dict(&guard, "W")
                .or_else(|| extract_int_from_dict(&guard, "Width"))
                .unwrap_or(1);
            let height = extract_int_from_dict(&guard, "H")
                .or_else(|| extract_int_from_dict(&guard, "Height"))
                .unwrap_or(1);
            (width.max(1), height.max(1))
        } else {
            (1, 1)
        }
    } else {
        (1, 1)
    };

    let image = Image::new(w, h, None);
    IMAGES.insert(image)
}

/// Check if a PDF image dictionary indicates a JPX (JPEG 2000) image.
///
/// A JPX image has /Filter set to /JPXDecode (or the abbreviation
/// /JBIG2Decode is NOT JPX, but /JPXDecode is).
#[unsafe(no_mangle)]
pub extern "C" fn pdf_is_jpx_image(_ctx: ContextHandle, dict: PdfObjHandle) -> i32 {
    if dict == 0 {
        return 0;
    }

    if let Some(arc) = PDF_OBJECTS.get(dict) {
        if let Ok(guard) = arc.lock() {
            // Check /Filter for JPXDecode
            if let Some(filter_name) = extract_name_from_dict(&guard, "Filter") {
                if filter_name == "JPXDecode" {
                    return 1;
                }
            }
            // Filter can also be an array of filter names
            if let Some(filter_obj) = extract_obj_from_dict(&guard, "Filter") {
                if let PdfObjType::Array(filters) = &filter_obj.obj_type {
                    for f in filters {
                        if let PdfObjType::Name(n) = &f.obj_type {
                            if n == "JPXDecode" {
                                return 1;
                            }
                        }
                    }
                }
            }
        }
    }
    0
}

/// Add an image to a document by creating a PDF image XObject
/// dictionary.
///
/// Reads the image dimensions and colorspace from the IMAGES handle
/// store and builds a PDF dictionary with /Type /XObject, /Subtype
/// /Image, /Width, /Height, /BitsPerComponent, and /ColorSpace.
/// Returns the handle of the newly created PDF dictionary object.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_add_image(
    _ctx: ContextHandle,
    _doc: DocumentHandle,
    image: ImageHandle,
) -> PdfObjHandle {
    if image == 0 {
        return 0;
    }

    let (w, h, cs_name) = if let Some(img_arc) = IMAGES.get(image) {
        if let Ok(guard) = img_arc.lock() {
            let cs = match guard.colorspace() {
                Some(cs) => cs.name().to_string(),
                None => "DeviceRGB".to_string(),
            };
            (guard.width(), guard.height(), cs)
        } else {
            return 0;
        }
    } else {
        return 0;
    };

    // Build the image XObject dictionary
    let mut entries = Vec::with_capacity(6);
    entries.push(("Type".to_string(), PdfObj::new_name("XObject")));
    entries.push(("Subtype".to_string(), PdfObj::new_name("Image")));
    entries.push(("Width".to_string(), PdfObj::new_int(w as i64)));
    entries.push(("Height".to_string(), PdfObj::new_int(h as i64)));
    entries.push(("BitsPerComponent".to_string(), PdfObj::new_int(8)));
    entries.push(("ColorSpace".to_string(), PdfObj::new_name(&cs_name)));

    let dict = PdfObj {
        obj_type: PdfObjType::Dict(entries),
        marked: false,
        dirty: false,
        parent_num: 0,
        refs: 1,
    };
    PDF_OBJECTS.insert(dict)
}

/// Add a colorspace to a document by creating a PDF colorspace
/// object (name or array).
///
/// For device colorspaces (Gray, RGB, CMYK, Lab), creates a simple
/// name object.  For custom colorspaces (ICC, Indexed, Separation),
/// creates an appropriate array representation.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_add_colorspace(
    _ctx: ContextHandle,
    _doc: DocumentHandle,
    cs: ColorspaceHandle,
) -> PdfObjHandle {
    if cs == 0 {
        return 0;
    }

    // Device colorspaces: return a simple name object
    let name = match cs {
        FZ_COLORSPACE_GRAY => Some("DeviceGray"),
        FZ_COLORSPACE_RGB => Some("DeviceRGB"),
        FZ_COLORSPACE_CMYK => Some("DeviceCMYK"),
        FZ_COLORSPACE_LAB => Some("Lab"),
        _ => None,
    };

    if let Some(n) = name {
        return PDF_OBJECTS.insert(PdfObj::new_name(n));
    }

    // Custom colorspaces (handle >= 100)
    if cs >= 100 {
        let real_handle = cs - 100;
        if let Some(cs_arc) = COLORSPACES.get(real_handle) {
            if let Ok(guard) = cs_arc.lock() {
                let cs_name = match guard.cs_type {
                    ColorspaceType::Indexed => {
                        // [/Indexed base hival lookup]
                        let mut arr = Vec::with_capacity(4);
                        arr.push(PdfObj::new_name("Indexed"));
                        // Base colorspace as name
                        let base_name = match guard.base_cs {
                            FZ_COLORSPACE_GRAY => "DeviceGray",
                            FZ_COLORSPACE_RGB => "DeviceRGB",
                            FZ_COLORSPACE_CMYK => "DeviceCMYK",
                            _ => "DeviceRGB",
                        };
                        arr.push(PdfObj::new_name(base_name));
                        arr.push(PdfObj::new_int(guard.high as i64));
                        arr.push(PdfObj::new_string(&guard.lookup));
                        let array_obj = PdfObj {
                            obj_type: PdfObjType::Array(arr),
                            marked: false,
                            dirty: false,
                            parent_num: 0,
                            refs: 1,
                        };
                        return PDF_OBJECTS.insert(array_obj);
                    }
                    ColorspaceType::Separation => {
                        return PDF_OBJECTS.insert(PdfObj::new_name(&guard.name));
                    }
                    ColorspaceType::Icc => {
                        return PDF_OBJECTS.insert(PdfObj::new_name(&guard.name));
                    }
                    _ => guard.name.clone(),
                };
                return PDF_OBJECTS.insert(PdfObj::new_name(&cs_name));
            }
        }
    }

    0
}

// ============================================================================
// FFI Functions - XObjects
// ============================================================================

/// Create a new Form XObject dictionary.
///
/// Builds a PDF dictionary with /Type /XObject, /Subtype /Form,
/// /BBox, /Matrix, /Resources, and stream data from the buffer.
/// Returns the handle of the newly created PDF dictionary object.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_xobject(
    _ctx: ContextHandle,
    _doc: DocumentHandle,
    bbox: *const f32,
    matrix: *const f32,
    res: PdfObjHandle,
    buffer: BufferHandle,
) -> PdfObjHandle {
    let mut entries = Vec::with_capacity(8);
    entries.push(("Type".to_string(), PdfObj::new_name("XObject")));
    entries.push(("Subtype".to_string(), PdfObj::new_name("Form")));
    entries.push(("FormType".to_string(), PdfObj::new_int(1)));

    // BBox array
    let bbox_vals = if !bbox.is_null() {
        unsafe { std::slice::from_raw_parts(bbox, 4) }
    } else {
        &[0.0f32, 0.0, 612.0, 792.0] // default letter size
    };
    let bbox_arr: Vec<PdfObj> = bbox_vals
        .iter()
        .map(|&v| PdfObj::new_real(v as f64))
        .collect();
    entries.push((
        "BBox".to_string(),
        PdfObj {
            obj_type: PdfObjType::Array(bbox_arr),
            marked: false,
            dirty: false,
            parent_num: 0,
            refs: 1,
        },
    ));

    // Matrix array
    let matrix_vals = if !matrix.is_null() {
        unsafe { std::slice::from_raw_parts(matrix, 6) }
    } else {
        &[1.0f32, 0.0, 0.0, 1.0, 0.0, 0.0] // identity
    };
    let matrix_arr: Vec<PdfObj> = matrix_vals
        .iter()
        .map(|&v| PdfObj::new_real(v as f64))
        .collect();
    entries.push((
        "Matrix".to_string(),
        PdfObj {
            obj_type: PdfObjType::Array(matrix_arr),
            marked: false,
            dirty: false,
            parent_num: 0,
            refs: 1,
        },
    ));

    // Resources reference
    if res != 0 {
        if let Some(res_arc) = PDF_OBJECTS.get(res) {
            if let Ok(guard) = res_arc.lock() {
                entries.push(("Resources".to_string(), guard.clone()));
            }
        }
    }

    // Stream data from buffer
    let stream_data = if buffer != 0 {
        if let Some(buf_arc) = BUFFERS.get(buffer) {
            if let Ok(guard) = buf_arc.lock() {
                guard.as_slice().to_vec()
            } else {
                Vec::new()
            }
        } else {
            Vec::new()
        }
    } else {
        Vec::new()
    };

    if !stream_data.is_empty() {
        entries.push((
            "Length".to_string(),
            PdfObj::new_int(stream_data.len() as i64),
        ));
    }

    let dict_obj = PdfObj {
        obj_type: PdfObjType::Dict(entries),
        marked: false,
        dirty: false,
        parent_num: 0,
        refs: 1,
    };

    PDF_OBJECTS.insert(dict_obj)
}

/// Update an existing Form XObject dictionary with new BBox,
/// Matrix, Resources, and stream data.
///
/// Modifies the dictionary entries in-place.  If the xobj handle
/// does not refer to a valid dictionary, this is a no-op.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_update_xobject(
    _ctx: ContextHandle,
    _doc: DocumentHandle,
    xobj: PdfObjHandle,
    bbox: *const f32,
    matrix: *const f32,
    res: PdfObjHandle,
    buffer: BufferHandle,
) {
    if xobj == 0 {
        return;
    }

    if let Some(arc) = PDF_OBJECTS.get(xobj) {
        if let Ok(mut guard) = arc.lock() {
            if let PdfObjType::Dict(ref mut entries) = guard.obj_type {
                // Update BBox
                if !bbox.is_null() {
                    let bbox_vals = unsafe { std::slice::from_raw_parts(bbox, 4) };
                    let bbox_arr: Vec<PdfObj> = bbox_vals
                        .iter()
                        .map(|&v| PdfObj::new_real(v as f64))
                        .collect();
                    let bbox_obj = PdfObj {
                        obj_type: PdfObjType::Array(bbox_arr),
                        marked: false,
                        dirty: false,
                        parent_num: 0,
                        refs: 1,
                    };
                    if let Some(entry) = entries.iter_mut().find(|(k, _)| k == "BBox") {
                        entry.1 = bbox_obj;
                    } else {
                        entries.push(("BBox".to_string(), bbox_obj));
                    }
                }

                // Update Matrix
                if !matrix.is_null() {
                    let matrix_vals = unsafe { std::slice::from_raw_parts(matrix, 6) };
                    let matrix_arr: Vec<PdfObj> = matrix_vals
                        .iter()
                        .map(|&v| PdfObj::new_real(v as f64))
                        .collect();
                    let matrix_obj = PdfObj {
                        obj_type: PdfObjType::Array(matrix_arr),
                        marked: false,
                        dirty: false,
                        parent_num: 0,
                        refs: 1,
                    };
                    if let Some(entry) = entries.iter_mut().find(|(k, _)| k == "Matrix") {
                        entry.1 = matrix_obj;
                    } else {
                        entries.push(("Matrix".to_string(), matrix_obj));
                    }
                }

                // Update Resources
                if res != 0 {
                    if let Some(res_arc) = PDF_OBJECTS.get(res) {
                        if let Ok(res_guard) = res_arc.lock() {
                            if let Some(entry) = entries.iter_mut().find(|(k, _)| k == "Resources")
                            {
                                entry.1 = res_guard.clone();
                            } else {
                                entries.push(("Resources".to_string(), res_guard.clone()));
                            }
                        }
                    }
                }

                // Update Length from buffer
                if buffer != 0 {
                    if let Some(buf_arc) = BUFFERS.get(buffer) {
                        if let Ok(buf_guard) = buf_arc.lock() {
                            let len = buf_guard.as_slice().len() as i64;
                            if let Some(entry) = entries.iter_mut().find(|(k, _)| k == "Length") {
                                entry.1 = PdfObj::new_int(len);
                            } else {
                                entries.push(("Length".to_string(), PdfObj::new_int(len)));
                            }
                        }
                    }
                }

                guard.dirty = true;
            }
        }
    }
}

/// Get the Resources dictionary from an XObject.
///
/// Reads the /Resources entry from the XObject dictionary and
/// returns a handle to it, or 0 if not present.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_xobject_resources(_ctx: ContextHandle, xobj: PdfObjHandle) -> PdfObjHandle {
    if xobj == 0 {
        return 0;
    }
    if let Some(arc) = PDF_OBJECTS.get(xobj) {
        if let Ok(guard) = arc.lock() {
            if let Some(res_obj) = extract_obj_from_dict(&guard, "Resources") {
                return PDF_OBJECTS.insert(res_obj.clone());
            }
        }
    }
    0
}

/// Get the BBox array from an XObject.
///
/// Reads the /BBox entry and writes four floats to the output
/// pointer.  If /BBox is missing, writes [0, 0, 0, 0].
#[unsafe(no_mangle)]
pub extern "C" fn pdf_xobject_bbox(_ctx: ContextHandle, xobj: PdfObjHandle, bbox: *mut f32) {
    if bbox.is_null() {
        return;
    }
    let mut vals = [0.0f32; 4];

    if xobj != 0 {
        if let Some(arc) = PDF_OBJECTS.get(xobj) {
            if let Ok(guard) = arc.lock() {
                if let Some(arr) = extract_float_array_from_dict(&guard, "BBox") {
                    for (i, &v) in arr.iter().take(4).enumerate() {
                        vals[i] = v;
                    }
                }
            }
        }
    }

    unsafe {
        *bbox.add(0) = vals[0];
        *bbox.add(1) = vals[1];
        *bbox.add(2) = vals[2];
        *bbox.add(3) = vals[3];
    }
}

/// Get the Matrix array from an XObject.
///
/// Reads the /Matrix entry and writes six floats to the output
/// pointer.  If /Matrix is missing, writes the identity matrix.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_xobject_matrix(_ctx: ContextHandle, xobj: PdfObjHandle, matrix: *mut f32) {
    if matrix.is_null() {
        return;
    }
    // Default: identity matrix
    let mut vals = [1.0f32, 0.0, 0.0, 1.0, 0.0, 0.0];

    if xobj != 0 {
        if let Some(arc) = PDF_OBJECTS.get(xobj) {
            if let Ok(guard) = arc.lock() {
                if let Some(arr) = extract_float_array_from_dict(&guard, "Matrix") {
                    if arr.len() >= 6 {
                        for (i, &v) in arr.iter().take(6).enumerate() {
                            vals[i] = v;
                        }
                    }
                }
            }
        }
    }

    unsafe {
        for i in 0..6 {
            *matrix.add(i) = vals[i];
        }
    }
}

/// Check if the XObject's transparency group is isolated.
///
/// Looks for /Group << /I true >> in the XObject dictionary.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_xobject_isolated(_ctx: ContextHandle, xobj: PdfObjHandle) -> i32 {
    if xobj == 0 {
        return 0;
    }
    if let Some(arc) = PDF_OBJECTS.get(xobj) {
        if let Ok(guard) = arc.lock() {
            if let Some(group) = extract_obj_from_dict(&guard, "Group") {
                if let Some(isolated) = extract_bool_from_dict(group, "I") {
                    return i32::from(isolated);
                }
            }
        }
    }
    0
}

/// Check if the XObject's transparency group uses knockout.
///
/// Looks for /Group << /K true >> in the XObject dictionary.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_xobject_knockout(_ctx: ContextHandle, xobj: PdfObjHandle) -> i32 {
    if xobj == 0 {
        return 0;
    }
    if let Some(arc) = PDF_OBJECTS.get(xobj) {
        if let Ok(guard) = arc.lock() {
            if let Some(group) = extract_obj_from_dict(&guard, "Group") {
                if let Some(knockout) = extract_bool_from_dict(group, "K") {
                    return i32::from(knockout);
                }
            }
        }
    }
    0
}

/// Check if the XObject has a transparency group.
///
/// Returns 1 if the XObject dictionary contains a /Group entry
/// with /S /Transparency, indicating the form XObject is part
/// of a transparency group.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_xobject_transparency(_ctx: ContextHandle, xobj: PdfObjHandle) -> i32 {
    if xobj == 0 {
        return 0;
    }
    if let Some(arc) = PDF_OBJECTS.get(xobj) {
        if let Ok(guard) = arc.lock() {
            if let Some(group) = extract_obj_from_dict(&guard, "Group") {
                // Check if /S is /Transparency
                if let Some(s) = extract_name_from_dict(group, "S") {
                    if s == "Transparency" {
                        return 1;
                    }
                }
                // Even without /S, having a /Group entry implies transparency
                return 1;
            }
        }
    }
    0
}

/// Get the colorspace from an XObject's transparency group.
///
/// Reads /Group << /CS ... >> and resolves it to a colorspace handle.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_xobject_colorspace(
    _ctx: ContextHandle,
    xobj: PdfObjHandle,
) -> ColorspaceHandle {
    if xobj == 0 {
        return 0;
    }
    if let Some(arc) = PDF_OBJECTS.get(xobj) {
        if let Ok(guard) = arc.lock() {
            if let Some(group) = extract_obj_from_dict(&guard, "Group") {
                if let Some(cs_obj) = extract_obj_from_dict(group, "CS") {
                    return load_colorspace_from_obj(cs_obj);
                }
            }
        }
    }
    0
}

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

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

    #[test]
    fn test_font_resource_key() {
        let key = FontResourceKey {
            digest: [0u8; 16],
            font_type: PDF_SIMPLE_FONT_RESOURCE,
            encoding: PDF_SIMPLE_ENCODING_LATIN,
            local_xref: 0,
        };
        assert_eq!(key.font_type, 1);
        assert_eq!(key.encoding, 0);
    }

    #[test]
    fn test_colorspace_resource_key() {
        let key = ColorspaceResourceKey {
            digest: [0u8; 16],
            local_xref: 0,
        };
        assert_eq!(key.local_xref, 0);
    }

    #[test]
    fn test_resource_stack() {
        let stack = ResourceStack::new();
        assert_eq!(stack.resources, 0);
        assert!(stack.next.is_none());

        let stack2 = ResourceStack::with_resources(123);
        assert_eq!(stack2.resources, 123);
    }

    #[test]
    fn test_pattern_new() {
        let p1 = Pattern::new();
        let p2 = Pattern::new();
        assert_ne!(p1.id, p2.id); // Unique IDs
        assert_eq!(p1.refs, 1);
        assert!(!p1.is_mask);
    }

    #[test]
    fn test_function_new() {
        let f = Function::new();
        assert_eq!(f.refs, 1);
        assert_eq!(f.func_type, FunctionType::Sampled);
        assert_eq!(f.n_inputs, 1);
        assert_eq!(f.n_outputs, 1);
    }

    #[test]
    fn test_function_exponential() {
        let mut f = Function::new();
        f.func_type = FunctionType::Exponential;
        f.n = 2.0;
        f.c0 = vec![0.0];
        f.c1 = vec![1.0];

        let mut output = [0.0f32; 1];
        f.eval(&[0.5], &mut output);
        assert!((output[0] - 0.25).abs() < 0.01); // 0.5^2 = 0.25
    }

    #[test]
    fn test_resource_tables() {
        let mut tables = ResourceTables::new();
        assert!(tables.fonts.is_empty());

        let digest = [1u8; 16];
        tables.fonts.insert(digest, 100);
        assert_eq!(tables.fonts.get(&digest), Some(&100));

        tables.clear();
        assert!(tables.fonts.is_empty());
    }

    #[test]
    fn test_ffi_resource_stack() {
        let ctx = 0;

        let stack = pdf_new_resource_stack(ctx, 100);
        assert!(stack > 0);

        let stack2 = pdf_push_resource_stack(ctx, stack, 200);
        assert!(stack2 > 0);
        assert_ne!(stack2, stack);

        let popped = pdf_pop_resource_stack(ctx, stack2);
        assert_eq!(popped, stack);

        pdf_drop_resource_stack(ctx, stack);
    }

    #[test]
    fn test_ffi_function() {
        let ctx = 0;

        let func = pdf_load_function(ctx, 0, 1, 3);
        assert!(func > 0);

        let kept = pdf_keep_function(ctx, func);
        assert_eq!(kept, func);

        let size = pdf_function_size(ctx, func);
        assert!(size > 0);

        let input = [0.5f32];
        let mut output = [0.0f32; 3];
        pdf_eval_function(ctx, func, input.as_ptr(), 1, output.as_mut_ptr(), 3);

        pdf_drop_function(ctx, func);
        pdf_drop_function(ctx, func);
    }

    #[test]
    fn test_ffi_pattern() {
        let ctx = 0;
        let doc = 123;

        let pat = pdf_load_pattern(ctx, doc, 0);
        assert!(pat > 0);

        assert_eq!(pdf_pattern_is_mask(ctx, pat), 0);
        assert_eq!(pdf_pattern_xstep(ctx, pat), 0.0);
        assert_eq!(pdf_pattern_ystep(ctx, pat), 0.0);

        let kept = pdf_keep_pattern(ctx, pat);
        assert_eq!(kept, pat);

        pdf_drop_pattern(ctx, pat);
        pdf_drop_pattern(ctx, pat);
    }

    #[test]
    fn test_ffi_font_resource() {
        let ctx = 0;
        let doc: DocumentHandle = 999;

        let mut key = FontResourceKey {
            digest: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
            font_type: PDF_SIMPLE_FONT_RESOURCE,
            encoding: PDF_SIMPLE_ENCODING_LATIN,
            local_xref: 0,
        };

        // Find should return 0 (not found)
        let found = pdf_find_font_resource(ctx, doc, 1, 0, 0, &mut key);
        assert_eq!(found, 0);

        // Insert resource
        let inserted = pdf_insert_font_resource(ctx, doc, &key, 42);
        assert_eq!(inserted, 42);

        // Now find should return 42
        let found2 = pdf_find_font_resource(ctx, doc, 1, 0, 0, &mut key);
        assert_eq!(found2, 42);

        // Clean up
        pdf_drop_resource_tables(ctx, doc);
    }

    #[test]
    fn test_ffi_colorspace_resource() {
        let ctx = 0;
        let doc: DocumentHandle = 888;

        let mut key = ColorspaceResourceKey {
            digest: [0xAA; 16],
            local_xref: 0,
        };

        // Insert resource
        let inserted = pdf_insert_colorspace_resource(ctx, doc, &key, 77);
        assert_eq!(inserted, 77);

        // Find should return 77
        let found = pdf_find_colorspace_resource(ctx, doc, 0, &mut key);
        assert_eq!(found, 77);

        // Clean up
        pdf_drop_resource_tables(ctx, doc);
    }

    #[test]
    fn test_ffi_empty_store() {
        let ctx = 0;
        let doc: DocumentHandle = 777;

        // Add some resources
        let key = FontResourceKey {
            digest: [0xBB; 16],
            font_type: 1,
            encoding: 0,
            local_xref: 0,
        };
        pdf_insert_font_resource(ctx, doc, &key, 55);

        // Empty the store
        pdf_empty_store(ctx, doc);

        // Resource should be gone
        let mut key2 = key;
        let found = pdf_find_font_resource(ctx, doc, 1, 0, 0, &mut key2);
        assert_eq!(found, 0);
    }
}
