//! FFI bindings for pdf_interpret (Content Stream Processor)
//!
//! Provides PDF content stream processing capabilities:
//! - Processor base with operator callbacks
//! - Run processor (rendering)
//! - Buffer processor (collect to buffer)
//! - Output processor (write to stream)
//! - Sanitize filter (clean content)
//! - Color filter (recolor content)
//! - Content stream processing

use crate::ffi::ffi_safety::{cstr_to_str, raw_to_slice, write_out};
use crate::ffi::{BUFFERS, Handle, HandleStore, STREAMS};
use crate::fitz::geometry::{Matrix, Rect};
use std::collections::HashMap;
use std::ffi::{CStr, CString, c_char, c_void};
use std::ptr;
use std::sync::LazyLock;

// ============================================================================
// Types and Enums
// ============================================================================

/// PDF processor requirements flags
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ProcessorRequirements {
    #[default]
    None = 0,
    /// Processor requires decoded images
    RequiresDecodedImages = 1,
}

/// Cull types for content filtering
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CullType {
    #[default]
    PathDrop = 0,
    PathFill = 1,
    PathStroke = 2,
    PathFillStroke = 3,
    ClipPathDrop = 4,
    ClipPathFill = 5,
    ClipPathStroke = 6,
    ClipPathFillStroke = 7,
    Glyph = 8,
    Image = 9,
    Shading = 10,
}

impl From<i32> for CullType {
    fn from(value: i32) -> Self {
        match value {
            0 => CullType::PathDrop,
            1 => CullType::PathFill,
            2 => CullType::PathStroke,
            3 => CullType::PathFillStroke,
            4 => CullType::ClipPathDrop,
            5 => CullType::ClipPathFill,
            6 => CullType::ClipPathStroke,
            7 => CullType::ClipPathFillStroke,
            8 => CullType::Glyph,
            9 => CullType::Image,
            10 => CullType::Shading,
            _ => CullType::PathDrop,
        }
    }
}

/// Processor type
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ProcessorType {
    #[default]
    Base = 0,
    Run = 1,
    Buffer = 2,
    Output = 3,
    Sanitize = 4,
    Color = 5,
    Vectorize = 6,
}

// ============================================================================
// Graphics State
// ============================================================================

/// Graphics state for PDF processing
#[derive(Debug, Clone, Default)]
pub struct PdfGstate {
    /// Current transformation matrix
    pub ctm: Matrix,
    /// Line width
    pub line_width: f32,
    /// Line join style (0=miter, 1=round, 2=bevel)
    pub line_join: i32,
    /// Line cap style (0=butt, 1=round, 2=square)
    pub line_cap: i32,
    /// Miter limit
    pub miter_limit: f32,
    /// Flatness
    pub flatness: f32,
    /// Dash array
    pub dash_array: Vec<f32>,
    /// Dash phase
    pub dash_phase: f32,
    /// Rendering intent
    pub rendering_intent: String,
    /// Blend mode
    pub blend_mode: String,
    /// Stroke alpha
    pub stroke_alpha: f32,
    /// Fill alpha
    pub fill_alpha: f32,
    /// Stroke color
    pub stroke_color: Vec<f32>,
    /// Fill color
    pub fill_color: Vec<f32>,
    /// Stroke colorspace name
    pub stroke_cs: String,
    /// Fill colorspace name
    pub fill_cs: String,
    /// Overprint stroke
    pub op_stroke: bool,
    /// Overprint fill
    pub op_fill: bool,
    /// Overprint mode
    pub opm: i32,
}

impl PdfGstate {
    pub fn new() -> Self {
        Self {
            ctm: Matrix::IDENTITY,
            line_width: 1.0,
            line_join: 0,
            line_cap: 0,
            miter_limit: 10.0,
            flatness: 1.0,
            dash_array: Vec::new(),
            dash_phase: 0.0,
            rendering_intent: "RelativeColorimetric".to_string(),
            blend_mode: "Normal".to_string(),
            stroke_alpha: 1.0,
            fill_alpha: 1.0,
            stroke_color: vec![0.0],
            fill_color: vec![0.0],
            stroke_cs: "DeviceGray".to_string(),
            fill_cs: "DeviceGray".to_string(),
            op_stroke: false,
            op_fill: false,
            opm: 0,
        }
    }
}

// ============================================================================
// Path State
// ============================================================================

/// Path element types
#[derive(Debug, Clone)]
pub enum PathElement {
    MoveTo {
        x: f32,
        y: f32,
    },
    LineTo {
        x: f32,
        y: f32,
    },
    CurveTo {
        x1: f32,
        y1: f32,
        x2: f32,
        y2: f32,
        x3: f32,
        y3: f32,
    },
    CurveV {
        x2: f32,
        y2: f32,
        x3: f32,
        y3: f32,
    },
    CurveY {
        x1: f32,
        y1: f32,
        x3: f32,
        y3: f32,
    },
    ClosePath,
    Rect {
        x: f32,
        y: f32,
        w: f32,
        h: f32,
    },
}

/// Current path being constructed
#[derive(Debug, Clone, Default)]
pub struct PathState {
    pub elements: Vec<PathElement>,
}

// ============================================================================
// Text State
// ============================================================================

/// Text state for PDF processing
#[derive(Debug, Clone, Default)]
pub struct PdfTextState {
    /// Character spacing
    pub char_space: f32,
    /// Word spacing
    pub word_space: f32,
    /// Horizontal scaling (percentage)
    pub scale: f32,
    /// Leading
    pub leading: f32,
    /// Font name
    pub font_name: String,
    /// Font handle
    pub font: Handle,
    /// Font size
    pub size: f32,
    /// Text rendering mode (0-7)
    pub render: i32,
    /// Text rise
    pub rise: f32,
}

impl PdfTextState {
    pub fn new() -> Self {
        Self {
            char_space: 0.0,
            word_space: 0.0,
            scale: 100.0,
            leading: 0.0,
            font_name: String::new(),
            font: 0,
            size: 12.0,
            render: 0,
            rise: 0.0,
        }
    }
}

/// Text object state
#[derive(Debug, Clone, Default)]
pub struct PdfTextObjectState {
    /// Text line matrix
    pub tlm: Matrix,
    /// Text matrix
    pub tm: Matrix,
    /// Text mode
    pub text_mode: i32,
    /// Accumulated text
    pub text_content: Vec<TextSpan>,
    /// Text bounding box
    pub text_bbox: Rect,
}

/// A span of text with positioning
#[derive(Debug, Clone)]
pub struct TextSpan {
    pub text: String,
    pub matrix: Matrix,
    pub font_name: String,
    pub font_size: f32,
}

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

/// Resource stack for content stream interpretation
#[derive(Debug, Clone, Default)]
pub struct ResourceStack {
    pub resources: Vec<Handle>,
}

// ============================================================================
// Processor Structure
// ============================================================================

/// PDF processor for content stream interpretation
#[derive(Default)]
pub struct PdfProcessor {
    /// Reference count
    pub refs: i32,
    /// Whether processor is closed
    pub closed: bool,
    /// Processor type
    pub proc_type: ProcessorType,
    /// Requirements
    pub requirements: ProcessorRequirements,
    /// Usage string (e.g., "View", "Print")
    pub usage: String,
    /// Hidden content handling
    pub hidden: i32,
    /// Resource stack
    pub rstack: ResourceStack,
    /// Chained processor
    pub chain: Option<Handle>,
    /// Graphics state stack
    pub gstate_stack: Vec<PdfGstate>,
    /// Current graphics state
    pub gstate: PdfGstate,
    /// Current path
    pub path: PathState,
    /// Text state
    pub text_state: PdfTextState,
    /// Text object state
    pub text_object: Option<PdfTextObjectState>,
    /// In text object (between BT and ET)
    pub in_text: bool,
    /// Output buffer (for buffer processor)
    pub output_buffer: Option<Handle>,
    /// Output stream (for output processor)
    pub output_stream: Option<Handle>,
    /// Device (for run processor)
    pub device: Option<Handle>,
    /// Document reference
    pub document: Option<Handle>,
    /// ASCII hex encode images
    pub ahx_encode: bool,
    /// Add newlines after operators
    pub newlines: bool,
    /// Collected operators (for debugging/inspection)
    pub operators: Vec<ProcessedOperator>,
    /// Struct parent
    pub struct_parent: i32,
    /// Transform
    pub transform: Matrix,
}

/// A processed operator with its arguments
#[derive(Debug, Clone)]
pub struct ProcessedOperator {
    pub name: String,
    pub args: Vec<OperatorArg>,
}

/// Operator argument types
#[derive(Debug, Clone)]
pub enum OperatorArg {
    Number(f32),
    Integer(i32),
    String(String),
    Name(String),
    Array(Vec<OperatorArg>),
    Boolean(bool),
}

// ============================================================================
// Global State
// ============================================================================

pub static PDF_PROCESSORS: LazyLock<HandleStore<PdfProcessor>> = LazyLock::new(HandleStore::new);
pub static PDF_GSTATES: LazyLock<HandleStore<PdfGstate>> = LazyLock::new(HandleStore::new);

// ============================================================================
// Processor Implementation
// ============================================================================

impl PdfProcessor {
    pub fn new(proc_type: ProcessorType) -> Self {
        Self {
            refs: 1,
            closed: false,
            proc_type,
            gstate: PdfGstate::new(),
            gstate_stack: Vec::new(),
            text_state: PdfTextState::new(),
            ..Default::default()
        }
    }

    /// Push current graphics state
    pub fn push_gstate(&mut self) {
        self.gstate_stack.push(self.gstate.clone());
    }

    /// Pop graphics state
    pub fn pop_gstate(&mut self) -> bool {
        if let Some(gstate) = self.gstate_stack.pop() {
            self.gstate = gstate;
            true
        } else {
            false
        }
    }

    /// Record an operator
    pub fn record_op(&mut self, name: &str, args: Vec<OperatorArg>) {
        self.operators.push(ProcessedOperator {
            name: name.to_string(),
            args,
        });
    }

    /// Write operator to output
    pub fn write_op(&self, _name: &str, _args: &[OperatorArg]) {
        // Would write to output_buffer or output_stream
    }

    /// Apply a parsed operator to this processor's state.
    ///
    /// Dispatches a PDF content stream operator (by keyword) with its
    /// accumulated operands to the appropriate state-mutation and
    /// recording methods.  Operators that are not recognized are
    /// recorded verbatim so round-tripping is lossless.
    fn apply_operator(&mut self, keyword: &str, operands: &[OperatorArg]) {
        match keyword {
            // ---- Graphics state ----
            "q" => {
                self.push_gstate();
                self.record_op("q", vec![]);
            }
            "Q" => {
                self.pop_gstate();
                self.record_op("Q", vec![]);
            }
            "cm" => {
                if operands.len() >= 6 {
                    let vals = extract_numbers(operands, 6);
                    let m = Matrix {
                        a: vals[0],
                        b: vals[1],
                        c: vals[2],
                        d: vals[3],
                        e: vals[4],
                        f: vals[5],
                    };
                    self.gstate.ctm = self.gstate.ctm.concat(&m);
                }
                self.record_op("cm", operands.to_vec());
            }
            "w" => {
                if let Some(v) = extract_number(operands, 0) {
                    self.gstate.line_width = v;
                }
                self.record_op("w", operands.to_vec());
            }
            "J" => {
                if let Some(v) = extract_int(operands, 0) {
                    self.gstate.line_cap = v;
                }
                self.record_op("J", operands.to_vec());
            }
            "j" => {
                if let Some(v) = extract_int(operands, 0) {
                    self.gstate.line_join = v;
                }
                self.record_op("j", operands.to_vec());
            }
            "M" => {
                if let Some(v) = extract_number(operands, 0) {
                    self.gstate.miter_limit = v;
                }
                self.record_op("M", operands.to_vec());
            }
            "d" => {
                // d expects [array] phase — recorded verbatim
                self.record_op("d", operands.to_vec());
            }
            "ri" => {
                if let Some(name) = extract_name(operands, 0) {
                    self.gstate.rendering_intent = name;
                }
                self.record_op("ri", operands.to_vec());
            }
            "i" => {
                if let Some(v) = extract_number(operands, 0) {
                    self.gstate.flatness = v;
                }
                self.record_op("i", operands.to_vec());
            }
            "gs" => {
                self.record_op("gs", operands.to_vec());
            }

            // ---- Path construction ----
            "m" => {
                if operands.len() >= 2 {
                    let x = extract_number(operands, 0).unwrap_or(0.0);
                    let y = extract_number(operands, 1).unwrap_or(0.0);
                    self.path.elements.push(PathElement::MoveTo { x, y });
                }
                self.record_op("m", operands.to_vec());
            }
            "l" => {
                if operands.len() >= 2 {
                    let x = extract_number(operands, 0).unwrap_or(0.0);
                    let y = extract_number(operands, 1).unwrap_or(0.0);
                    self.path.elements.push(PathElement::LineTo { x, y });
                }
                self.record_op("l", operands.to_vec());
            }
            "c" => {
                if operands.len() >= 6 {
                    let v = extract_numbers(operands, 6);
                    self.path.elements.push(PathElement::CurveTo {
                        x1: v[0],
                        y1: v[1],
                        x2: v[2],
                        y2: v[3],
                        x3: v[4],
                        y3: v[5],
                    });
                }
                self.record_op("c", operands.to_vec());
            }
            "v" => {
                if operands.len() >= 4 {
                    let v = extract_numbers(operands, 4);
                    self.path.elements.push(PathElement::CurveV {
                        x2: v[0],
                        y2: v[1],
                        x3: v[2],
                        y3: v[3],
                    });
                }
                self.record_op("v", operands.to_vec());
            }
            "y" => {
                if operands.len() >= 4 {
                    let v = extract_numbers(operands, 4);
                    self.path.elements.push(PathElement::CurveY {
                        x1: v[0],
                        y1: v[1],
                        x3: v[2],
                        y3: v[3],
                    });
                }
                self.record_op("y", operands.to_vec());
            }
            "h" => {
                self.path.elements.push(PathElement::ClosePath);
                self.record_op("h", vec![]);
            }
            "re" => {
                if operands.len() >= 4 {
                    let v = extract_numbers(operands, 4);
                    self.path.elements.push(PathElement::Rect {
                        x: v[0],
                        y: v[1],
                        w: v[2],
                        h: v[3],
                    });
                }
                self.record_op("re", operands.to_vec());
            }

            // ---- Path painting ----
            "S" => {
                self.record_op("S", vec![]);
                self.path = PathState::default();
            }
            "s" => {
                self.path.elements.push(PathElement::ClosePath);
                self.record_op("s", vec![]);
                self.path = PathState::default();
            }
            "f" | "F" => {
                self.record_op("f", vec![]);
                self.path = PathState::default();
            }
            "f*" => {
                self.record_op("f*", vec![]);
                self.path = PathState::default();
            }
            "B" => {
                self.record_op("B", vec![]);
                self.path = PathState::default();
            }
            "B*" => {
                self.record_op("B*", vec![]);
                self.path = PathState::default();
            }
            "b" => {
                self.path.elements.push(PathElement::ClosePath);
                self.record_op("b", vec![]);
                self.path = PathState::default();
            }
            "b*" => {
                self.path.elements.push(PathElement::ClosePath);
                self.record_op("b*", vec![]);
                self.path = PathState::default();
            }
            "n" => {
                self.record_op("n", vec![]);
                self.path = PathState::default();
            }

            // ---- Clipping ----
            "W" => self.record_op("W", vec![]),
            "W*" => self.record_op("W*", vec![]),

            // ---- Text objects ----
            "BT" => {
                self.in_text = true;
                self.text_object = Some(PdfTextObjectState {
                    tlm: Matrix::IDENTITY,
                    tm: Matrix::IDENTITY,
                    text_mode: 0,
                    text_content: Vec::new(),
                    text_bbox: Rect::default(),
                });
                self.record_op("BT", vec![]);
            }
            "ET" => {
                self.in_text = false;
                self.text_object = None;
                self.record_op("ET", vec![]);
            }

            // ---- Text state ----
            "Tc" => {
                if let Some(v) = extract_number(operands, 0) {
                    self.text_state.char_space = v;
                }
                self.record_op("Tc", operands.to_vec());
            }
            "Tw" => {
                if let Some(v) = extract_number(operands, 0) {
                    self.text_state.word_space = v;
                }
                self.record_op("Tw", operands.to_vec());
            }
            "Tz" => {
                if let Some(v) = extract_number(operands, 0) {
                    self.text_state.scale = v;
                }
                self.record_op("Tz", operands.to_vec());
            }
            "TL" => {
                if let Some(v) = extract_number(operands, 0) {
                    self.text_state.leading = v;
                }
                self.record_op("TL", operands.to_vec());
            }
            "Tf" => {
                if let Some(name) = extract_name(operands, 0) {
                    self.text_state.font_name = name;
                }
                if let Some(v) = extract_number(operands, 1) {
                    self.text_state.size = v;
                }
                self.record_op("Tf", operands.to_vec());
            }
            "Tr" => {
                if let Some(v) = extract_int(operands, 0) {
                    self.text_state.render = v;
                }
                self.record_op("Tr", operands.to_vec());
            }
            "Ts" => {
                if let Some(v) = extract_number(operands, 0) {
                    self.text_state.rise = v;
                }
                self.record_op("Ts", operands.to_vec());
            }

            // ---- Text positioning ----
            "Td" => {
                if operands.len() >= 2 {
                    let tx = extract_number(operands, 0).unwrap_or(0.0);
                    let ty = extract_number(operands, 1).unwrap_or(0.0);
                    if let Some(ref mut tos) = self.text_object {
                        let translate = Matrix {
                            a: 1.0,
                            b: 0.0,
                            c: 0.0,
                            d: 1.0,
                            e: tx,
                            f: ty,
                        };
                        tos.tlm = tos.tlm.concat(&translate);
                        tos.tm = tos.tlm;
                    }
                }
                self.record_op("Td", operands.to_vec());
            }
            "TD" => {
                if operands.len() >= 2 {
                    let tx = extract_number(operands, 0).unwrap_or(0.0);
                    let ty = extract_number(operands, 1).unwrap_or(0.0);
                    self.text_state.leading = -ty;
                    if let Some(ref mut tos) = self.text_object {
                        let translate = Matrix {
                            a: 1.0,
                            b: 0.0,
                            c: 0.0,
                            d: 1.0,
                            e: tx,
                            f: ty,
                        };
                        tos.tlm = tos.tlm.concat(&translate);
                        tos.tm = tos.tlm;
                    }
                }
                self.record_op("TD", operands.to_vec());
            }
            "Tm" => {
                if operands.len() >= 6 {
                    let vals = extract_numbers(operands, 6);
                    if let Some(ref mut tos) = self.text_object {
                        tos.tlm = Matrix {
                            a: vals[0],
                            b: vals[1],
                            c: vals[2],
                            d: vals[3],
                            e: vals[4],
                            f: vals[5],
                        };
                        tos.tm = tos.tlm;
                    }
                }
                self.record_op("Tm", operands.to_vec());
            }
            "T*" => {
                let leading = self.text_state.leading;
                if let Some(ref mut tos) = self.text_object {
                    let translate = Matrix {
                        a: 1.0,
                        b: 0.0,
                        c: 0.0,
                        d: 1.0,
                        e: 0.0,
                        f: -leading,
                    };
                    tos.tlm = tos.tlm.concat(&translate);
                    tos.tm = tos.tlm;
                }
                self.record_op("T*", vec![]);
            }

            // ---- Text showing ----
            "Tj" => {
                let text = match operands.first() {
                    Some(OperatorArg::String(s)) => s.clone(),
                    _ => String::new(),
                };
                let font_name = self.text_state.font_name.clone();
                let font_size = self.text_state.size;
                if let Some(ref mut tos) = self.text_object {
                    tos.text_content.push(TextSpan {
                        text: text.clone(),
                        matrix: tos.tm,
                        font_name,
                        font_size,
                    });
                }
                self.record_op("Tj", operands.to_vec());
            }
            "TJ" => {
                self.record_op("TJ", operands.to_vec());
            }
            "'" => {
                // T* then Tj
                self.apply_operator("T*", &[]);
                // Re-record as the composite ' operator
                self.operators.pop(); // remove the T* we just pushed
                let text = match operands.first() {
                    Some(OperatorArg::String(s)) => s.clone(),
                    _ => String::new(),
                };
                let font_name = self.text_state.font_name.clone();
                let font_size = self.text_state.size;
                if let Some(ref mut tos) = self.text_object {
                    tos.text_content.push(TextSpan {
                        text: text.clone(),
                        matrix: tos.tm,
                        font_name,
                        font_size,
                    });
                }
                self.record_op("'", operands.to_vec());
            }
            "\"" => {
                // Set word spacing, char spacing, then '
                if let Some(aw) = extract_number(operands, 0) {
                    self.text_state.word_space = aw;
                }
                if let Some(ac) = extract_number(operands, 1) {
                    self.text_state.char_space = ac;
                }
                self.apply_operator("T*", &[]);
                self.operators.pop(); // remove the T* we just pushed
                let text = match operands.get(2) {
                    Some(OperatorArg::String(s)) => s.clone(),
                    _ => String::new(),
                };
                let font_name = self.text_state.font_name.clone();
                let font_size = self.text_state.size;
                if let Some(ref mut tos) = self.text_object {
                    tos.text_content.push(TextSpan {
                        text: text.clone(),
                        matrix: tos.tm,
                        font_name,
                        font_size,
                    });
                }
                self.record_op("\"", operands.to_vec());
            }

            // ---- Color ----
            "CS" => {
                if let Some(name) = extract_name(operands, 0) {
                    self.gstate.stroke_cs = name;
                }
                self.record_op("CS", operands.to_vec());
            }
            "cs" => {
                if let Some(name) = extract_name(operands, 0) {
                    self.gstate.fill_cs = name;
                }
                self.record_op("cs", operands.to_vec());
            }
            "SC" | "SCN" => {
                let colors: Vec<f32> = operands
                    .iter()
                    .filter_map(|a| match a {
                        OperatorArg::Number(n) => Some(*n),
                        OperatorArg::Integer(n) => Some(*n as f32),
                        _ => None,
                    })
                    .collect();
                if !colors.is_empty() {
                    self.gstate.stroke_color = colors;
                }
                self.record_op(keyword, operands.to_vec());
            }
            "sc" | "scn" => {
                let colors: Vec<f32> = operands
                    .iter()
                    .filter_map(|a| match a {
                        OperatorArg::Number(n) => Some(*n),
                        OperatorArg::Integer(n) => Some(*n as f32),
                        _ => None,
                    })
                    .collect();
                if !colors.is_empty() {
                    self.gstate.fill_color = colors;
                }
                self.record_op(keyword, operands.to_vec());
            }
            "G" => {
                if let Some(g) = extract_number(operands, 0) {
                    self.gstate.stroke_cs = "DeviceGray".to_string();
                    self.gstate.stroke_color = vec![g];
                }
                self.record_op("G", operands.to_vec());
            }
            "g" => {
                if let Some(g) = extract_number(operands, 0) {
                    self.gstate.fill_cs = "DeviceGray".to_string();
                    self.gstate.fill_color = vec![g];
                }
                self.record_op("g", operands.to_vec());
            }
            "RG" => {
                if operands.len() >= 3 {
                    let v = extract_numbers(operands, 3);
                    self.gstate.stroke_cs = "DeviceRGB".to_string();
                    self.gstate.stroke_color = v;
                }
                self.record_op("RG", operands.to_vec());
            }
            "rg" => {
                if operands.len() >= 3 {
                    let v = extract_numbers(operands, 3);
                    self.gstate.fill_cs = "DeviceRGB".to_string();
                    self.gstate.fill_color = v;
                }
                self.record_op("rg", operands.to_vec());
            }
            "K" => {
                if operands.len() >= 4 {
                    let v = extract_numbers(operands, 4);
                    self.gstate.stroke_cs = "DeviceCMYK".to_string();
                    self.gstate.stroke_color = v;
                }
                self.record_op("K", operands.to_vec());
            }
            "k" => {
                if operands.len() >= 4 {
                    let v = extract_numbers(operands, 4);
                    self.gstate.fill_cs = "DeviceCMYK".to_string();
                    self.gstate.fill_color = v;
                }
                self.record_op("k", operands.to_vec());
            }

            // ---- XObject / inline image ----
            "Do" | "sh" | "BI" => {
                self.record_op(keyword, operands.to_vec());
            }

            // ---- Type 3 font operators ----
            "d0" | "d1" => {
                self.record_op(keyword, operands.to_vec());
            }

            // ---- Marked content ----
            "MP" | "DP" | "BMC" | "BDC" | "EMC" => {
                self.record_op(keyword, operands.to_vec());
            }

            // ---- Compatibility ----
            "BX" | "EX" => self.record_op(keyword, vec![]),

            // ---- Unknown operator: record verbatim ----
            _ => {
                self.record_op(keyword, operands.to_vec());
            }
        }
    }
}

// ============================================================================
// Content Stream Tokenizer
// ============================================================================

/// Extract a floating-point value from an operand at the given index.
fn extract_number(operands: &[OperatorArg], idx: usize) -> Option<f32> {
    operands.get(idx).and_then(|a| match a {
        OperatorArg::Number(n) => Some(*n),
        OperatorArg::Integer(n) => Some(*n as f32),
        _ => None,
    })
}

/// Extract an integer value from an operand at the given index.
fn extract_int(operands: &[OperatorArg], idx: usize) -> Option<i32> {
    operands.get(idx).and_then(|a| match a {
        OperatorArg::Integer(n) => Some(*n),
        OperatorArg::Number(n) => Some(*n as i32),
        _ => None,
    })
}

/// Extract a name string from an operand at the given index.
fn extract_name(operands: &[OperatorArg], idx: usize) -> Option<String> {
    operands.get(idx).and_then(|a| match a {
        OperatorArg::Name(s) => Some(s.clone()),
        OperatorArg::String(s) => Some(s.clone()),
        _ => None,
    })
}

/// Extract `count` floating-point values from the front of the operand list.
fn extract_numbers(operands: &[OperatorArg], count: usize) -> Vec<f32> {
    (0..count)
        .map(|i| extract_number(operands, i).unwrap_or(0.0))
        .collect()
}

/// A single token from a PDF content stream.
#[derive(Debug, Clone)]
enum CsToken {
    /// A numeric literal (integer or real).
    Number(f32),
    /// An integer literal (kept separate for operator-arg fidelity).
    Integer(i32),
    /// A PDF name object (leading '/' already stripped).
    Name(String),
    /// A parenthesized string literal.
    StringLiteral(String),
    /// A hex string literal.
    HexString(String),
    /// A keyword (operator name or boolean).
    Keyword(String),
}

/// Tokenize a PDF content stream byte slice into a sequence of tokens.
///
/// Handles the standard PDF lexical conventions: whitespace and
/// delimiters, number literals (integer and real), name objects,
/// literal strings (balanced parentheses, escape sequences), hex
/// strings, and keywords (including multi-character operators like
/// `f*`, `B*`, `b*`, `W*`, `T*`).
fn tokenize_content_stream(data: &[u8]) -> Vec<CsToken> {
    let mut tokens = Vec::new();
    let len = data.len();
    let mut pos = 0;

    while pos < len {
        let b = data[pos];

        // Skip whitespace (space, tab, CR, LF, form feed, null)
        if b == b' ' || b == b'\t' || b == b'\r' || b == b'\n' || b == b'\x0C' || b == b'\0' {
            pos += 1;
            continue;
        }

        // Skip comments
        if b == b'%' {
            while pos < len && data[pos] != b'\n' && data[pos] != b'\r' {
                pos += 1;
            }
            continue;
        }

        // Literal string
        if b == b'(' {
            pos += 1;
            let mut s = Vec::new();
            let mut depth = 1u32;
            while pos < len && depth > 0 {
                match data[pos] {
                    b'(' => {
                        depth += 1;
                        s.push(b'(');
                    }
                    b')' => {
                        depth -= 1;
                        if depth > 0 {
                            s.push(b')');
                        }
                    }
                    b'\\' => {
                        pos += 1;
                        if pos < len {
                            match data[pos] {
                                b'n' => s.push(b'\n'),
                                b'r' => s.push(b'\r'),
                                b't' => s.push(b'\t'),
                                b'b' => s.push(0x08),
                                b'f' => s.push(0x0C),
                                b'(' => s.push(b'('),
                                b')' => s.push(b')'),
                                b'\\' => s.push(b'\\'),
                                d @ b'0'..=b'7' => {
                                    // Octal escape: 1-3 digits
                                    let mut val = (d - b'0') as u32;
                                    for _ in 0..2 {
                                        if pos + 1 < len
                                            && data[pos + 1] >= b'0'
                                            && data[pos + 1] <= b'7'
                                        {
                                            pos += 1;
                                            val = val * 8 + (data[pos] - b'0') as u32;
                                        } else {
                                            break;
                                        }
                                    }
                                    s.push((val & 0xFF) as u8);
                                }
                                // Line continuation — skip
                                b'\r' => {
                                    if pos + 1 < len && data[pos + 1] == b'\n' {
                                        pos += 1;
                                    }
                                }
                                b'\n' => {}
                                other => s.push(other),
                            }
                        }
                    }
                    other => s.push(other),
                }
                pos += 1;
            }
            tokens.push(CsToken::StringLiteral(
                String::from_utf8_lossy(&s).to_string(),
            ));
            continue;
        }

        // Hex string
        if b == b'<' && (pos + 1 >= len || data[pos + 1] != b'<') {
            pos += 1;
            let mut hex = Vec::new();
            while pos < len && data[pos] != b'>' {
                let h = data[pos];
                if h.is_ascii_hexdigit() {
                    hex.push(h);
                }
                pos += 1;
            }
            if pos < len {
                pos += 1; // skip '>'
            }
            // Pad odd-length hex strings with trailing 0
            if hex.len() % 2 != 0 {
                hex.push(b'0');
            }
            let decoded: Vec<u8> = hex
                .chunks(2)
                .filter_map(|pair| {
                    let hi = char::from(pair[0]).to_digit(16)?;
                    let lo = char::from(pair[1]).to_digit(16)?;
                    Some((hi * 16 + lo) as u8)
                })
                .collect();
            tokens.push(CsToken::HexString(
                String::from_utf8_lossy(&decoded).to_string(),
            ));
            continue;
        }

        // Name object
        if b == b'/' {
            pos += 1;
            let start = pos;
            while pos < len {
                let ch = data[pos];
                if ch == b' '
                    || ch == b'\t'
                    || ch == b'\r'
                    || ch == b'\n'
                    || ch == b'\x0C'
                    || ch == b'\0'
                    || ch == b'/'
                    || ch == b'('
                    || ch == b')'
                    || ch == b'<'
                    || ch == b'>'
                    || ch == b'['
                    || ch == b']'
                    || ch == b'{'
                    || ch == b'}'
                    || ch == b'%'
                {
                    break;
                }
                pos += 1;
            }
            let name = String::from_utf8_lossy(&data[start..pos]).to_string();
            tokens.push(CsToken::Name(name));
            continue;
        }

        // Skip array/dict delimiters (record as keyword for pass-through)
        if b == b'[' || b == b']' {
            tokens.push(CsToken::Keyword(String::from(b as char)));
            pos += 1;
            continue;
        }
        // Dict delimiters << >>
        if b == b'<' && pos + 1 < len && data[pos + 1] == b'<' {
            tokens.push(CsToken::Keyword("<<".to_string()));
            pos += 2;
            continue;
        }
        if b == b'>' && pos + 1 < len && data[pos + 1] == b'>' {
            tokens.push(CsToken::Keyword(">>".to_string()));
            pos += 2;
            continue;
        }

        // Number (integer or real)
        if b == b'+' || b == b'-' || b == b'.' || b.is_ascii_digit() {
            let start = pos;
            let mut has_dot = b == b'.';
            pos += 1;
            while pos < len {
                let ch = data[pos];
                if ch == b'.' && !has_dot {
                    has_dot = true;
                    pos += 1;
                } else if ch.is_ascii_digit() {
                    pos += 1;
                } else {
                    break;
                }
            }
            let num_str = String::from_utf8_lossy(&data[start..pos]);
            if has_dot {
                let val = num_str.parse::<f32>().unwrap_or(0.0);
                tokens.push(CsToken::Number(val));
            } else {
                match num_str.parse::<i32>() {
                    Ok(iv) => tokens.push(CsToken::Integer(iv)),
                    Err(_) => {
                        let val = num_str.parse::<f32>().unwrap_or(0.0);
                        tokens.push(CsToken::Number(val));
                    }
                }
            }
            continue;
        }

        // Keyword (operator or boolean)
        if b.is_ascii_alphabetic() || b == b'\'' || b == b'"' {
            let start = pos;
            pos += 1;
            while pos < len {
                let ch = data[pos];
                if ch.is_ascii_alphanumeric() || ch == b'*' || ch == b'\'' || ch == b'"' {
                    pos += 1;
                } else {
                    break;
                }
            }
            let kw = String::from_utf8_lossy(&data[start..pos]).to_string();
            // "true" and "false" are booleans in PDF
            match kw.as_str() {
                "true" => tokens.push(CsToken::Keyword("true".to_string())),
                "false" => tokens.push(CsToken::Keyword("false".to_string())),
                _ => tokens.push(CsToken::Keyword(kw)),
            }
            continue;
        }

        // Skip any unrecognized byte
        pos += 1;
    }

    tokens
}

/// Known PDF content stream operator keywords.
///
/// Used by the tokenizer-to-operator dispatcher to distinguish
/// operator keywords from operand tokens that happen to be keywords
/// (e.g. `true`, `false`, `null`).
fn is_operator_keyword(kw: &str) -> bool {
    matches!(
        kw,
        "q" | "Q"
            | "cm"
            | "w"
            | "J"
            | "j"
            | "M"
            | "d"
            | "ri"
            | "i"
            | "gs"
            | "m"
            | "l"
            | "c"
            | "v"
            | "y"
            | "h"
            | "re"
            | "S"
            | "s"
            | "f"
            | "F"
            | "f*"
            | "B"
            | "B*"
            | "b"
            | "b*"
            | "n"
            | "W"
            | "W*"
            | "BT"
            | "ET"
            | "Tc"
            | "Tw"
            | "Tz"
            | "TL"
            | "Tf"
            | "Tr"
            | "Ts"
            | "Td"
            | "TD"
            | "Tm"
            | "T*"
            | "Tj"
            | "TJ"
            | "'"
            | "\""
            | "d0"
            | "d1"
            | "CS"
            | "cs"
            | "SC"
            | "SCN"
            | "sc"
            | "scn"
            | "G"
            | "g"
            | "RG"
            | "rg"
            | "K"
            | "k"
            | "Do"
            | "sh"
            | "BI"
            | "ID"
            | "EI"
            | "MP"
            | "DP"
            | "BMC"
            | "BDC"
            | "EMC"
            | "BX"
            | "EX"
            | "true"
            | "false"
            | "null"
    )
}

/// Convert a sequence of content-stream tokens into a series of
/// (operator-keyword, operands) pairs.  Tokens are accumulated as
/// operands until an operator keyword is encountered, at which point
/// the accumulated operands are associated with that operator.
fn tokens_to_operators(tokens: &[CsToken]) -> Vec<(String, Vec<OperatorArg>)> {
    let mut result = Vec::new();
    let mut operands: Vec<OperatorArg> = Vec::new();

    for tok in tokens {
        match tok {
            CsToken::Keyword(kw) if is_operator_keyword(kw) => {
                // "true" / "false" / "null" can appear as operands in
                // certain contexts (e.g. inside inline images) but in
                // the general content stream they are not operators.
                match kw.as_str() {
                    "true" => {
                        operands.push(OperatorArg::Boolean(true));
                    }
                    "false" => {
                        operands.push(OperatorArg::Boolean(false));
                    }
                    "null" => {
                        // Treat as integer 0 for compatibility
                        operands.push(OperatorArg::Integer(0));
                    }
                    _ => {
                        result.push((kw.clone(), std::mem::take(&mut operands)));
                    }
                }
            }
            // Accumulate operands
            CsToken::Number(n) => operands.push(OperatorArg::Number(*n)),
            CsToken::Integer(n) => operands.push(OperatorArg::Integer(*n)),
            CsToken::Name(s) => operands.push(OperatorArg::Name(s.clone())),
            CsToken::StringLiteral(s) => operands.push(OperatorArg::String(s.clone())),
            CsToken::HexString(s) => operands.push(OperatorArg::String(s.clone())),
            CsToken::Keyword(kw) => {
                // Unknown keyword — treat as name-like operand
                operands.push(OperatorArg::Name(kw.clone()));
            }
        }
    }
    result
}

/// Retrieve the raw byte content from a stream handle.
///
/// Tries the STREAMS store first, then falls back to the BUFFERS
/// store, returning `None` if neither contains the handle.
fn get_stream_data(stm: Handle) -> Option<Vec<u8>> {
    if let Some(stream_arc) = STREAMS.get(stm) {
        let guard = stream_arc.lock().unwrap();
        return Some(guard.data.clone());
    }
    if let Some(buf_arc) = BUFFERS.get(stm) {
        let guard = buf_arc.lock().unwrap();
        return Some(guard.data().to_vec());
    }
    None
}

/// Parse and execute a content stream on a processor.
///
/// Tokenizes the raw byte data, converts the token sequence into
/// (operator, operands) pairs, and dispatches each operator to
/// the processor's `apply_operator` method.
fn process_stream_on_processor(proc: &mut PdfProcessor, data: &[u8]) {
    let tokens = tokenize_content_stream(data);
    let ops = tokens_to_operators(&tokens);
    for (keyword, operands) in &ops {
        proc.apply_operator(keyword, operands);
    }
}

// ============================================================================
// FFI Functions - Processor Lifecycle
// ============================================================================

/// Create a new processor
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_processor(_ctx: Handle, size: i32) -> Handle {
    let proc_type = match size {
        1 => ProcessorType::Run,
        2 => ProcessorType::Buffer,
        3 => ProcessorType::Output,
        4 => ProcessorType::Sanitize,
        5 => ProcessorType::Color,
        6 => ProcessorType::Vectorize,
        _ => ProcessorType::Base,
    };
    let processor = PdfProcessor::new(proc_type);
    PDF_PROCESSORS.insert(processor)
}

/// Keep (increment ref count) a processor
#[unsafe(no_mangle)]
pub extern "C" fn pdf_keep_processor(_ctx: Handle, proc: Handle) -> Handle {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.refs += 1;
    }
    proc
}

/// Close a processor
#[unsafe(no_mangle)]
pub extern "C" fn pdf_close_processor(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.closed = true;

        // Close chained processor if any
        if let Some(chain) = proc_guard.chain {
            drop(proc_guard);
            pdf_close_processor(_ctx, chain);
        }
    }
}

/// Drop a processor
#[unsafe(no_mangle)]
pub extern "C" fn pdf_drop_processor(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.refs -= 1;
        if proc_guard.refs <= 0 {
            // Drop chained processor
            let chain = proc_guard.chain;
            drop(proc_guard);

            if let Some(chain_handle) = chain {
                pdf_drop_processor(_ctx, chain_handle);
            }
            PDF_PROCESSORS.remove(proc);
        }
    }
}

/// Reset a processor for reuse
#[unsafe(no_mangle)]
pub extern "C" fn pdf_reset_processor(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.closed = false;
        proc_guard.gstate = PdfGstate::new();
        proc_guard.gstate_stack.clear();
        proc_guard.path = PathState::default();
        proc_guard.text_state = PdfTextState::new();
        proc_guard.text_object = None;
        proc_guard.in_text = false;
        proc_guard.operators.clear();
    }
}

// ============================================================================
// FFI Functions - Processor Factories
// ============================================================================

/// Create a run processor for rendering
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_run_processor(
    _ctx: Handle,
    doc: Handle,
    dev: Handle,
    a: f32,
    b: f32,
    c: f32,
    d: f32,
    e: f32,
    f: f32,
    struct_parent: i32,
    usage: *const c_char,
) -> Handle {
    let mut processor = PdfProcessor::new(ProcessorType::Run);
    processor.document = Some(doc);
    processor.device = Some(dev);
    processor.transform = Matrix { a, b, c, d, e, f };
    processor.struct_parent = struct_parent;

    if !usage.is_null() {
        if let Ok(s) = unsafe { CStr::from_ptr(usage) }.to_str() {
            processor.usage = s.to_string();
        }
    }

    PDF_PROCESSORS.insert(processor)
}

/// Create a buffer processor
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_buffer_processor(
    _ctx: Handle,
    buffer: Handle,
    ahx_encode: i32,
    newlines: i32,
) -> Handle {
    let mut processor = PdfProcessor::new(ProcessorType::Buffer);
    processor.output_buffer = Some(buffer);
    processor.ahx_encode = ahx_encode != 0;
    processor.newlines = newlines != 0;

    PDF_PROCESSORS.insert(processor)
}

/// Create an output processor
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_output_processor(
    _ctx: Handle,
    out: Handle,
    ahx_encode: i32,
    newlines: i32,
) -> Handle {
    let mut processor = PdfProcessor::new(ProcessorType::Output);
    processor.output_stream = Some(out);
    processor.ahx_encode = ahx_encode != 0;
    processor.newlines = newlines != 0;

    PDF_PROCESSORS.insert(processor)
}

/// Create a sanitize filter processor
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_sanitize_filter(
    _ctx: Handle,
    _doc: Handle,
    chain: Handle,
    struct_parents: i32,
    a: f32,
    b: f32,
    c: f32,
    d: f32,
    e: f32,
    f: f32,
) -> Handle {
    let mut processor = PdfProcessor::new(ProcessorType::Sanitize);
    processor.chain = Some(chain);
    processor.struct_parent = struct_parents;
    processor.transform = Matrix { a, b, c, d, e, f };

    PDF_PROCESSORS.insert(processor)
}

/// Create a color filter processor
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_color_filter(
    _ctx: Handle,
    _doc: Handle,
    chain: Handle,
    struct_parents: i32,
    a: f32,
    b: f32,
    c: f32,
    d: f32,
    e: f32,
    f: f32,
) -> Handle {
    let mut processor = PdfProcessor::new(ProcessorType::Color);
    processor.chain = Some(chain);
    processor.struct_parent = struct_parents;
    processor.transform = Matrix { a, b, c, d, e, f };

    PDF_PROCESSORS.insert(processor)
}

/// Create a vectorize filter processor
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_vectorize_filter(
    _ctx: Handle,
    _doc: Handle,
    chain: Handle,
    struct_parents: i32,
    a: f32,
    b: f32,
    c: f32,
    d: f32,
    e: f32,
    f: f32,
) -> Handle {
    let mut processor = PdfProcessor::new(ProcessorType::Vectorize);
    processor.chain = Some(chain);
    processor.struct_parent = struct_parents;
    processor.transform = Matrix { a, b, c, d, e, f };

    PDF_PROCESSORS.insert(processor)
}

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

/// Push resources onto the processor's resource stack
#[unsafe(no_mangle)]
pub extern "C" fn pdf_processor_push_resources(_ctx: Handle, proc: Handle, res: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.rstack.resources.push(res);

        // Pass to chain if any
        if let Some(chain) = proc_guard.chain {
            drop(proc_guard);
            pdf_processor_push_resources(_ctx, chain, res);
        }
    }
}

/// Pop resources from the processor's resource stack
#[unsafe(no_mangle)]
pub extern "C" fn pdf_processor_pop_resources(_ctx: Handle, proc: Handle) -> Handle {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        let res = proc_guard.rstack.resources.pop().unwrap_or(0);

        // Pass to chain if any
        if let Some(chain) = proc_guard.chain {
            drop(proc_guard);
            let _ = pdf_processor_pop_resources(_ctx, chain);
        }

        return res;
    }
    0
}

// ============================================================================
// FFI Functions - Content Processing
// ============================================================================

/// Process a content stream.
///
/// Retrieves the raw bytes from the stream handle, tokenizes the
/// PDF content stream operators, and dispatches each operator to the
/// processor.  Graphics state is saved/restored around the entire
/// stream so the caller's state is unaffected.  The resource handle
/// is pushed onto the processor's resource stack for the duration of
/// processing so that operator handlers can resolve names (fonts,
/// XObjects, etc.).
#[unsafe(no_mangle)]
pub extern "C" fn pdf_process_contents(
    _ctx: Handle,
    proc: Handle,
    _doc: Handle,
    res: Handle,
    stm: Handle,
    out_res: *mut Handle,
) {
    // Retrieve stream data before acquiring the processor lock so
    // we never hold two locks simultaneously.
    let stream_data = get_stream_data(stm);

    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();

        // Push resources for name resolution during processing
        proc_guard.rstack.resources.push(res);

        // Save graphics state so the caller is unaffected
        proc_guard.push_gstate();

        // Tokenize and execute every operator in the content stream
        if let Some(data) = stream_data {
            process_stream_on_processor(&mut proc_guard, &data);
        }

        // Restore graphics state
        proc_guard.pop_gstate();

        // Pop resources
        proc_guard.rstack.resources.pop();
    }

    if !out_res.is_null() {
        unsafe { *out_res = res };
    }
}

/// Process an annotation.
///
/// Looks up the annotation from the ANNOTATIONS handle store, reads
/// its type, geometry, and visual properties, then synthesizes the
/// corresponding appearance-stream operators and applies them to the
/// processor.  Graphics state is saved/restored around the generated
/// operators so the page-level state is preserved.
///
/// If the annotation has color and geometry information, a simple
/// appearance stream is synthesized that draws the annotation's
/// visual representation (rectangle outline for Square/FreeText,
/// filled rectangle for Highlight, stroked lines for Underline /
/// StrikeOut, etc.).  Annotations whose visual appearance cannot be
/// synthesized (e.g. Widget, Sound) emit only the metadata operator.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_process_annot(_ctx: Handle, proc: Handle, annot: Handle) {
    use crate::ffi::annot::ANNOTATIONS;

    // Read annotation properties outside the processor lock
    let annot_info = ANNOTATIONS.get(annot).map(|arc| {
        let guard = arc.lock().unwrap();
        (
            guard.annot_type(),
            guard.rect(),
            guard.color(),
            guard.opacity(),
            guard.contents().to_string(),
            guard.interior_color().to_vec(),
        )
    });

    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();

        proc_guard.push_gstate();

        if let Some((atype, rect, color, opacity, _contents, interior_color)) = annot_info {
            // Record annotation metadata
            proc_guard.record_op(
                "annot",
                vec![
                    OperatorArg::Integer(annot as i32),
                    OperatorArg::Name(format!("{:?}", atype)),
                ],
            );

            // Set opacity via graphics state if not fully opaque
            if (opacity - 1.0).abs() > f32::EPSILON {
                proc_guard.gstate.fill_alpha = opacity;
                proc_guard.gstate.stroke_alpha = opacity;
            }

            // Synthesize appearance stream operators based on type
            match atype {
                crate::pdf::annot::AnnotType::Square | crate::pdf::annot::AnnotType::FreeText => {
                    // Set stroke color
                    if let Some([r, g, b]) = color {
                        proc_guard.apply_operator(
                            "RG",
                            &[
                                OperatorArg::Number(r),
                                OperatorArg::Number(g),
                                OperatorArg::Number(b),
                            ],
                        );
                    }
                    // Set fill color from interior color if available
                    if interior_color.len() >= 3 {
                        proc_guard.apply_operator(
                            "rg",
                            &[
                                OperatorArg::Number(interior_color[0]),
                                OperatorArg::Number(interior_color[1]),
                                OperatorArg::Number(interior_color[2]),
                            ],
                        );
                    }
                    // Draw rectangle
                    proc_guard.apply_operator(
                        "re",
                        &[
                            OperatorArg::Number(rect.x0),
                            OperatorArg::Number(rect.y0),
                            OperatorArg::Number(rect.x1 - rect.x0),
                            OperatorArg::Number(rect.y1 - rect.y0),
                        ],
                    );
                    // Stroke (or fill+stroke if interior color present)
                    if interior_color.len() >= 3 {
                        proc_guard.apply_operator("B", &[]);
                    } else {
                        proc_guard.apply_operator("S", &[]);
                    }
                }
                crate::pdf::annot::AnnotType::Circle => {
                    if let Some([r, g, b]) = color {
                        proc_guard.apply_operator(
                            "RG",
                            &[
                                OperatorArg::Number(r),
                                OperatorArg::Number(g),
                                OperatorArg::Number(b),
                            ],
                        );
                    }
                    // Approximate circle with four cubic Bezier curves
                    let cx = (rect.x0 + rect.x1) / 2.0;
                    let cy = (rect.y0 + rect.y1) / 2.0;
                    let rx = (rect.x1 - rect.x0) / 2.0;
                    let ry = (rect.y1 - rect.y0) / 2.0;
                    // Magic number for cubic approximation of quarter-circle
                    let k: f32 = 0.5522848;
                    let kx = rx * k;
                    let ky = ry * k;

                    proc_guard.apply_operator(
                        "m",
                        &[OperatorArg::Number(cx + rx), OperatorArg::Number(cy)],
                    );
                    proc_guard.apply_operator(
                        "c",
                        &[
                            OperatorArg::Number(cx + rx),
                            OperatorArg::Number(cy + ky),
                            OperatorArg::Number(cx + kx),
                            OperatorArg::Number(cy + ry),
                            OperatorArg::Number(cx),
                            OperatorArg::Number(cy + ry),
                        ],
                    );
                    proc_guard.apply_operator(
                        "c",
                        &[
                            OperatorArg::Number(cx - kx),
                            OperatorArg::Number(cy + ry),
                            OperatorArg::Number(cx - rx),
                            OperatorArg::Number(cy + ky),
                            OperatorArg::Number(cx - rx),
                            OperatorArg::Number(cy),
                        ],
                    );
                    proc_guard.apply_operator(
                        "c",
                        &[
                            OperatorArg::Number(cx - rx),
                            OperatorArg::Number(cy - ky),
                            OperatorArg::Number(cx - kx),
                            OperatorArg::Number(cy - ry),
                            OperatorArg::Number(cx),
                            OperatorArg::Number(cy - ry),
                        ],
                    );
                    proc_guard.apply_operator(
                        "c",
                        &[
                            OperatorArg::Number(cx + kx),
                            OperatorArg::Number(cy - ry),
                            OperatorArg::Number(cx + rx),
                            OperatorArg::Number(cy - ky),
                            OperatorArg::Number(cx + rx),
                            OperatorArg::Number(cy),
                        ],
                    );
                    proc_guard.apply_operator("S", &[]);
                }
                crate::pdf::annot::AnnotType::Highlight => {
                    if let Some([r, g, b]) = color {
                        proc_guard.apply_operator(
                            "rg",
                            &[
                                OperatorArg::Number(r),
                                OperatorArg::Number(g),
                                OperatorArg::Number(b),
                            ],
                        );
                    }
                    proc_guard.apply_operator(
                        "re",
                        &[
                            OperatorArg::Number(rect.x0),
                            OperatorArg::Number(rect.y0),
                            OperatorArg::Number(rect.x1 - rect.x0),
                            OperatorArg::Number(rect.y1 - rect.y0),
                        ],
                    );
                    proc_guard.apply_operator("f", &[]);
                }
                crate::pdf::annot::AnnotType::Underline => {
                    if let Some([r, g, b]) = color {
                        proc_guard.apply_operator(
                            "RG",
                            &[
                                OperatorArg::Number(r),
                                OperatorArg::Number(g),
                                OperatorArg::Number(b),
                            ],
                        );
                    }
                    // Draw line at bottom of rect
                    proc_guard.apply_operator(
                        "m",
                        &[OperatorArg::Number(rect.x0), OperatorArg::Number(rect.y0)],
                    );
                    proc_guard.apply_operator(
                        "l",
                        &[OperatorArg::Number(rect.x1), OperatorArg::Number(rect.y0)],
                    );
                    proc_guard.apply_operator("S", &[]);
                }
                crate::pdf::annot::AnnotType::StrikeOut => {
                    if let Some([r, g, b]) = color {
                        proc_guard.apply_operator(
                            "RG",
                            &[
                                OperatorArg::Number(r),
                                OperatorArg::Number(g),
                                OperatorArg::Number(b),
                            ],
                        );
                    }
                    // Draw line through middle of rect
                    let mid_y = (rect.y0 + rect.y1) / 2.0;
                    proc_guard.apply_operator(
                        "m",
                        &[OperatorArg::Number(rect.x0), OperatorArg::Number(mid_y)],
                    );
                    proc_guard.apply_operator(
                        "l",
                        &[OperatorArg::Number(rect.x1), OperatorArg::Number(mid_y)],
                    );
                    proc_guard.apply_operator("S", &[]);
                }
                crate::pdf::annot::AnnotType::Line => {
                    if let Some([r, g, b]) = color {
                        proc_guard.apply_operator(
                            "RG",
                            &[
                                OperatorArg::Number(r),
                                OperatorArg::Number(g),
                                OperatorArg::Number(b),
                            ],
                        );
                    }
                    // Draw diagonal line across the rect
                    proc_guard.apply_operator(
                        "m",
                        &[OperatorArg::Number(rect.x0), OperatorArg::Number(rect.y0)],
                    );
                    proc_guard.apply_operator(
                        "l",
                        &[OperatorArg::Number(rect.x1), OperatorArg::Number(rect.y1)],
                    );
                    proc_guard.apply_operator("S", &[]);
                }
                _ => {
                    // For annotation types without a synthesizable
                    // appearance (Text, Popup, Widget, etc.), the
                    // metadata "annot" operator above is sufficient.
                }
            }
        } else {
            // Annotation handle not found — record a bare marker so
            // the processor chain knows an annotation was attempted.
            proc_guard.record_op("annot", vec![OperatorArg::Integer(annot as i32)]);
        }

        proc_guard.pop_gstate();
    }
}

/// Process a glyph content stream for Type 3 fonts.
///
/// Type 3 fonts define each glyph as a small content stream that
/// uses the same operator set as page content (path construction,
/// painting, colour, etc.) plus the d0 / d1 glyph-metric operators.
///
/// This function pushes the provided resource handle, saves the
/// graphics state, retrieves the glyph stream data from the
/// STREAMS / BUFFERS store, tokenizes and executes it, then
/// restores state and pops resources.  The resource handle is
/// typically the Font resource dictionary of the Type 3 font so
/// that nested name lookups resolve correctly.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_process_glyph(_ctx: Handle, proc: Handle, _doc: Handle, res: Handle) {
    // The resource handle doubles as the stream handle for glyph
    // descriptions — the caller passes the glyph CharProc stream.
    let stream_data = get_stream_data(res);

    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();

        // Push glyph resources
        proc_guard.rstack.resources.push(res);

        // Save graphics state for isolation
        proc_guard.push_gstate();

        // Tokenize and execute the glyph content stream
        if let Some(data) = stream_data {
            process_stream_on_processor(&mut proc_guard, &data);
        }

        // Restore graphics state
        proc_guard.pop_gstate();

        // Pop glyph resources
        proc_guard.rstack.resources.pop();
    }
}

/// Process raw contents without resource or graphics-state management.
///
/// Unlike `pdf_process_contents`, this function does not push/pop
/// resources or save/restore the graphics state.  It simply
/// tokenizes the content stream and records every operator.  This
/// is useful when the caller needs fine-grained control over state
/// management (e.g. when stitching multiple streams together or
/// when the stream has already been partially processed).
#[unsafe(no_mangle)]
pub extern "C" fn pdf_process_raw_contents(_ctx: Handle, proc: Handle, _doc: Handle, stm: Handle) {
    // Retrieve stream bytes before acquiring the processor lock
    let stream_data = get_stream_data(stm);

    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();

        if let Some(data) = stream_data {
            process_stream_on_processor(&mut proc_guard, &data);
        }
    }
}

/// Count the q/Q imbalance in a content stream.
///
/// Scans the content stream for `q` (save) and `Q` (restore)
/// operators and determines how many extra `q` operators must be
/// prepended and how many extra `Q` operators must be appended to
/// make the stream balanced.
///
/// A balanced stream has equal numbers of `q` and `Q` operators and
/// never underflows (i.e. a `Q` never appears without a matching
/// prior `q`).
///
/// `prepend` receives the number of `q` operators to insert before
/// the stream (to absorb orphan leading `Q`s).  `append` receives
/// the number of `Q` operators to insert after the stream (to close
/// unclosed `q`s).
#[unsafe(no_mangle)]
pub extern "C" fn pdf_count_q_balance(
    _ctx: Handle,
    _doc: Handle,
    _res: Handle,
    stm: Handle,
    prepend: *mut i32,
    append: *mut i32,
) {
    let mut missing_q: i32 = 0; // Extra q's needed at the start
    let mut depth: i32 = 0; // Current nesting depth

    if let Some(data) = get_stream_data(stm) {
        let tokens = tokenize_content_stream(&data);
        let ops = tokens_to_operators(&tokens);

        for (keyword, _) in &ops {
            match keyword.as_str() {
                "q" => {
                    depth += 1;
                }
                "Q" => {
                    if depth > 0 {
                        depth -= 1;
                    } else {
                        // Q without a matching q — we need to prepend
                        // an extra q to absorb it.
                        missing_q += 1;
                    }
                }
                _ => {}
            }
        }
    }

    // depth  = number of unclosed q's at the end  → need that many Q's appended
    // missing_q = number of unmatched Q's at start → need that many q's prepended
    if !prepend.is_null() {
        unsafe { *prepend = missing_q };
    }
    if !append.is_null() {
        unsafe { *append = depth };
    }
}

// ============================================================================
// FFI Functions - Graphics State Operators
// ============================================================================

/// Set line width (w operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_w(_ctx: Handle, proc: Handle, linewidth: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.line_width = linewidth;
        proc_guard.record_op("w", vec![OperatorArg::Number(linewidth)]);
    }
}

/// Set line join (j operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_j(_ctx: Handle, proc: Handle, linejoin: i32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.line_join = linejoin;
        proc_guard.record_op("j", vec![OperatorArg::Integer(linejoin)]);
    }
}

/// Set line cap (J operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_J(_ctx: Handle, proc: Handle, linecap: i32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.line_cap = linecap;
        proc_guard.record_op("J", vec![OperatorArg::Integer(linecap)]);
    }
}

/// Set miter limit (M operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_M(_ctx: Handle, proc: Handle, miterlimit: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.miter_limit = miterlimit;
        proc_guard.record_op("M", vec![OperatorArg::Number(miterlimit)]);
    }
}

/// Set dash pattern (d operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_d(
    _ctx: Handle,
    proc: Handle,
    array: *const f32,
    array_len: i32,
    phase: f32,
) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();

        let dash_array = if !array.is_null() && array_len > 0 {
            unsafe { std::slice::from_raw_parts(array, array_len as usize) }.to_vec()
        } else {
            Vec::new()
        };

        proc_guard.gstate.dash_array = dash_array.clone();
        proc_guard.gstate.dash_phase = phase;

        let args: Vec<OperatorArg> = dash_array.iter().map(|&v| OperatorArg::Number(v)).collect();
        proc_guard.record_op(
            "d",
            vec![OperatorArg::Array(args), OperatorArg::Number(phase)],
        );
    }
}

/// Set rendering intent (ri operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_ri(_ctx: Handle, proc: Handle, intent: *const c_char) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();

        let intent_str = if !intent.is_null() {
            unsafe { CStr::from_ptr(intent) }
                .to_str()
                .unwrap_or("RelativeColorimetric")
                .to_string()
        } else {
            "RelativeColorimetric".to_string()
        };

        proc_guard.gstate.rendering_intent = intent_str.clone();
        proc_guard.record_op("ri", vec![OperatorArg::Name(intent_str)]);
    }
}

/// Set flatness (i operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_i(_ctx: Handle, proc: Handle, flatness: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.flatness = flatness;
        proc_guard.record_op("i", vec![OperatorArg::Number(flatness)]);
    }
}

/// Save graphics state (q operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_q(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.push_gstate();
        proc_guard.record_op("q", vec![]);
    }
}

/// Restore graphics state (Q operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_Q(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.pop_gstate();
        proc_guard.record_op("Q", vec![]);
    }
}

/// Concatenate matrix (cm operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_cm(
    _ctx: Handle,
    proc: Handle,
    a: f32,
    b: f32,
    c: f32,
    d: f32,
    e: f32,
    f: f32,
) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();

        let new_matrix = Matrix { a, b, c, d, e, f };
        proc_guard.gstate.ctm = proc_guard.gstate.ctm.concat(&new_matrix);

        proc_guard.record_op(
            "cm",
            vec![
                OperatorArg::Number(a),
                OperatorArg::Number(b),
                OperatorArg::Number(c),
                OperatorArg::Number(d),
                OperatorArg::Number(e),
                OperatorArg::Number(f),
            ],
        );
    }
}

// ============================================================================
// FFI Functions - Extended Graphics State Operators
// ============================================================================

/// Begin extended graphics state
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_gs_begin(_ctx: Handle, proc: Handle, name: *const c_char) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        let name_str = if !name.is_null() {
            unsafe { CStr::from_ptr(name) }.to_str().unwrap_or("")
        } else {
            ""
        };
        proc_guard.record_op("gs", vec![OperatorArg::Name(name_str.to_string())]);
    }
}

/// Set blend mode
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_gs_BM(_ctx: Handle, proc: Handle, blendmode: *const c_char) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        let mode = if !blendmode.is_null() {
            unsafe { CStr::from_ptr(blendmode) }
                .to_str()
                .unwrap_or("Normal")
                .to_string()
        } else {
            "Normal".to_string()
        };
        proc_guard.gstate.blend_mode = mode;
    }
}

/// Set fill alpha (ca)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_gs_ca(_ctx: Handle, proc: Handle, alpha: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.fill_alpha = alpha;
    }
}

/// Set stroke alpha (CA)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_gs_CA(_ctx: Handle, proc: Handle, alpha: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.stroke_alpha = alpha;
    }
}

/// End extended graphics state
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_gs_end(_ctx: Handle, _proc: Handle) {
    // End of ExtGState processing
}

/// Set overprint for fill (op)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_gs_op(_ctx: Handle, proc: Handle, b: i32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.op_fill = b != 0;
    }
}

/// Set overprint for stroke (OP)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_gs_OP(_ctx: Handle, proc: Handle, b: i32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.op_stroke = b != 0;
    }
}

/// Set overprint mode (OPM)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_gs_OPM(_ctx: Handle, proc: Handle, i: i32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.opm = i;
    }
}

// ============================================================================
// FFI Functions - Path Construction Operators
// ============================================================================

/// Move to (m operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_m(_ctx: Handle, proc: Handle, x: f32, y: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.path.elements.push(PathElement::MoveTo { x, y });
        proc_guard.record_op("m", vec![OperatorArg::Number(x), OperatorArg::Number(y)]);
    }
}

/// Line to (l operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_l(_ctx: Handle, proc: Handle, x: f32, y: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.path.elements.push(PathElement::LineTo { x, y });
        proc_guard.record_op("l", vec![OperatorArg::Number(x), OperatorArg::Number(y)]);
    }
}

/// Curve to (c operator) - cubic bezier with two control points
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_c(
    _ctx: Handle,
    proc: Handle,
    x1: f32,
    y1: f32,
    x2: f32,
    y2: f32,
    x3: f32,
    y3: f32,
) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.path.elements.push(PathElement::CurveTo {
            x1,
            y1,
            x2,
            y2,
            x3,
            y3,
        });
        proc_guard.record_op(
            "c",
            vec![
                OperatorArg::Number(x1),
                OperatorArg::Number(y1),
                OperatorArg::Number(x2),
                OperatorArg::Number(y2),
                OperatorArg::Number(x3),
                OperatorArg::Number(y3),
            ],
        );
    }
}

/// Curve to (v operator) - control point at current point
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_v(_ctx: Handle, proc: Handle, x2: f32, y2: f32, x3: f32, y3: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard
            .path
            .elements
            .push(PathElement::CurveV { x2, y2, x3, y3 });
        proc_guard.record_op(
            "v",
            vec![
                OperatorArg::Number(x2),
                OperatorArg::Number(y2),
                OperatorArg::Number(x3),
                OperatorArg::Number(y3),
            ],
        );
    }
}

/// Curve to (y operator) - control point at end point
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_y(_ctx: Handle, proc: Handle, x1: f32, y1: f32, x3: f32, y3: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard
            .path
            .elements
            .push(PathElement::CurveY { x1, y1, x3, y3 });
        proc_guard.record_op(
            "y",
            vec![
                OperatorArg::Number(x1),
                OperatorArg::Number(y1),
                OperatorArg::Number(x3),
                OperatorArg::Number(y3),
            ],
        );
    }
}

/// Close path (h operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_h(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.path.elements.push(PathElement::ClosePath);
        proc_guard.record_op("h", vec![]);
    }
}

/// Rectangle (re operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_re(_ctx: Handle, proc: Handle, x: f32, y: f32, w: f32, h: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard
            .path
            .elements
            .push(PathElement::Rect { x, y, w, h });
        proc_guard.record_op(
            "re",
            vec![
                OperatorArg::Number(x),
                OperatorArg::Number(y),
                OperatorArg::Number(w),
                OperatorArg::Number(h),
            ],
        );
    }
}

// ============================================================================
// FFI Functions - Path Painting Operators
// ============================================================================

/// Stroke path (S operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_S(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("S", vec![]);
        proc_guard.path = PathState::default();
    }
}

/// Close and stroke (s operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_s(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.path.elements.push(PathElement::ClosePath);
        proc_guard.record_op("s", vec![]);
        proc_guard.path = PathState::default();
    }
}

/// Fill path nonzero (f operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_f(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("f", vec![]);
        proc_guard.path = PathState::default();
    }
}

/// Fill path nonzero (F operator - same as f)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_F(_ctx: Handle, proc: Handle) {
    pdf_op_f(_ctx, proc);
}

/// Fill path even-odd (f* operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_fstar(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("f*", vec![]);
        proc_guard.path = PathState::default();
    }
}

/// Fill and stroke nonzero (B operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_B(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("B", vec![]);
        proc_guard.path = PathState::default();
    }
}

/// Fill and stroke even-odd (B* operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_Bstar(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("B*", vec![]);
        proc_guard.path = PathState::default();
    }
}

/// Close, fill and stroke nonzero (b operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_b(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.path.elements.push(PathElement::ClosePath);
        proc_guard.record_op("b", vec![]);
        proc_guard.path = PathState::default();
    }
}

/// Close, fill and stroke even-odd (b* operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_bstar(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.path.elements.push(PathElement::ClosePath);
        proc_guard.record_op("b*", vec![]);
        proc_guard.path = PathState::default();
    }
}

/// End path without filling or stroking (n operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_n(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("n", vec![]);
        proc_guard.path = PathState::default();
    }
}

// ============================================================================
// FFI Functions - Clipping Operators
// ============================================================================

/// Set clipping path nonzero (W operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_W(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("W", vec![]);
    }
}

/// Set clipping path even-odd (W* operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_Wstar(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("W*", vec![]);
    }
}

// ============================================================================
// FFI Functions - Text Object Operators
// ============================================================================

/// Begin text object (BT operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_BT(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.in_text = true;
        proc_guard.text_object = Some(PdfTextObjectState {
            tlm: Matrix::IDENTITY,
            tm: Matrix::IDENTITY,
            text_mode: 0,
            text_content: Vec::new(),
            text_bbox: Rect::default(),
        });
        proc_guard.record_op("BT", vec![]);
    }
}

/// End text object (ET operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_ET(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.in_text = false;
        proc_guard.text_object = None;
        proc_guard.record_op("ET", vec![]);
    }
}

// ============================================================================
// FFI Functions - Text State Operators
// ============================================================================

/// Set character spacing (Tc operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_Tc(_ctx: Handle, proc: Handle, charspace: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.text_state.char_space = charspace;
        proc_guard.record_op("Tc", vec![OperatorArg::Number(charspace)]);
    }
}

/// Set word spacing (Tw operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_Tw(_ctx: Handle, proc: Handle, wordspace: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.text_state.word_space = wordspace;
        proc_guard.record_op("Tw", vec![OperatorArg::Number(wordspace)]);
    }
}

/// Set horizontal scaling (Tz operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_Tz(_ctx: Handle, proc: Handle, scale: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.text_state.scale = scale;
        proc_guard.record_op("Tz", vec![OperatorArg::Number(scale)]);
    }
}

/// Set leading (TL operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_TL(_ctx: Handle, proc: Handle, leading: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.text_state.leading = leading;
        proc_guard.record_op("TL", vec![OperatorArg::Number(leading)]);
    }
}

/// Set font and size (Tf operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_Tf(_ctx: Handle, proc: Handle, name: *const c_char, size: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();

        let font_name = if !name.is_null() {
            unsafe { CStr::from_ptr(name) }
                .to_str()
                .unwrap_or("")
                .to_string()
        } else {
            String::new()
        };

        proc_guard.text_state.font_name = font_name.clone();
        proc_guard.text_state.size = size;
        proc_guard.record_op(
            "Tf",
            vec![OperatorArg::Name(font_name), OperatorArg::Number(size)],
        );
    }
}

/// Set text rendering mode (Tr operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_Tr(_ctx: Handle, proc: Handle, render: i32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.text_state.render = render;
        proc_guard.record_op("Tr", vec![OperatorArg::Integer(render)]);
    }
}

/// Set text rise (Ts operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_Ts(_ctx: Handle, proc: Handle, rise: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.text_state.rise = rise;
        proc_guard.record_op("Ts", vec![OperatorArg::Number(rise)]);
    }
}

// ============================================================================
// FFI Functions - Text Positioning Operators
// ============================================================================

/// Move text position (Td operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_Td(_ctx: Handle, proc: Handle, tx: f32, ty: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        if let Some(ref mut tos) = proc_guard.text_object {
            let translate = Matrix {
                a: 1.0,
                b: 0.0,
                c: 0.0,
                d: 1.0,
                e: tx,
                f: ty,
            };
            tos.tlm = tos.tlm.concat(&translate);
            tos.tm = tos.tlm;
        }
        proc_guard.record_op("Td", vec![OperatorArg::Number(tx), OperatorArg::Number(ty)]);
    }
}

/// Move text position and set leading (TD operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_TD(_ctx: Handle, proc: Handle, tx: f32, ty: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.text_state.leading = -ty;
        if let Some(ref mut tos) = proc_guard.text_object {
            let translate = Matrix {
                a: 1.0,
                b: 0.0,
                c: 0.0,
                d: 1.0,
                e: tx,
                f: ty,
            };
            tos.tlm = tos.tlm.concat(&translate);
            tos.tm = tos.tlm;
        }
        proc_guard.record_op("TD", vec![OperatorArg::Number(tx), OperatorArg::Number(ty)]);
    }
}

/// Set text matrix (Tm operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_Tm(
    _ctx: Handle,
    proc: Handle,
    a: f32,
    b: f32,
    c: f32,
    d: f32,
    e: f32,
    f: f32,
) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        if let Some(ref mut tos) = proc_guard.text_object {
            tos.tlm = Matrix { a, b, c, d, e, f };
            tos.tm = tos.tlm;
        }
        proc_guard.record_op(
            "Tm",
            vec![
                OperatorArg::Number(a),
                OperatorArg::Number(b),
                OperatorArg::Number(c),
                OperatorArg::Number(d),
                OperatorArg::Number(e),
                OperatorArg::Number(f),
            ],
        );
    }
}

/// Move to next line (T* operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_Tstar(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        let leading = proc_guard.text_state.leading;
        if let Some(ref mut tos) = proc_guard.text_object {
            let translate = Matrix {
                a: 1.0,
                b: 0.0,
                c: 0.0,
                d: 1.0,
                e: 0.0,
                f: -leading,
            };
            tos.tlm = tos.tlm.concat(&translate);
            tos.tm = tos.tlm;
        }
        proc_guard.record_op("T*", vec![]);
    }
}

// ============================================================================
// FFI Functions - Text Showing Operators
// ============================================================================

/// Show text (Tj operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_Tj(_ctx: Handle, proc: Handle, str: *const c_char, len: usize) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();

        let text = if !str.is_null() {
            let bytes = unsafe { std::slice::from_raw_parts(str as *const u8, len) };
            String::from_utf8_lossy(bytes).to_string()
        } else {
            String::new()
        };

        // Extract text state values before mutable borrow
        let font_name = proc_guard.text_state.font_name.clone();
        let font_size = proc_guard.text_state.size;

        if let Some(ref mut tos) = proc_guard.text_object {
            tos.text_content.push(TextSpan {
                text: text.clone(),
                matrix: tos.tm,
                font_name,
                font_size,
            });
        }

        proc_guard.record_op("Tj", vec![OperatorArg::String(text)]);
    }
}

/// Show text with individual positioning (TJ operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_TJ(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("TJ", vec![]);
    }
}

/// Move to next line and show text (' operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_squote(_ctx: Handle, proc: Handle, str: *const c_char, len: usize) {
    // T*
    pdf_op_Tstar(_ctx, proc);
    // Tj
    pdf_op_Tj(_ctx, proc, str, len);
}

/// Set spacing, move to next line and show text (" operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_dquote(
    _ctx: Handle,
    proc: Handle,
    aw: f32,
    ac: f32,
    str: *const c_char,
    len: usize,
) {
    // Tw
    pdf_op_Tw(_ctx, proc, aw);
    // Tc
    pdf_op_Tc(_ctx, proc, ac);
    // '
    pdf_op_squote(_ctx, proc, str, len);
}

// ============================================================================
// FFI Functions - Type 3 Font Operators
// ============================================================================

/// Type 3 font glyph width (d0 operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_d0(_ctx: Handle, proc: Handle, wx: f32, wy: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("d0", vec![OperatorArg::Number(wx), OperatorArg::Number(wy)]);
    }
}

/// Type 3 font glyph width and bounding box (d1 operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_d1(
    _ctx: Handle,
    proc: Handle,
    wx: f32,
    wy: f32,
    llx: f32,
    lly: f32,
    urx: f32,
    ury: f32,
) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op(
            "d1",
            vec![
                OperatorArg::Number(wx),
                OperatorArg::Number(wy),
                OperatorArg::Number(llx),
                OperatorArg::Number(lly),
                OperatorArg::Number(urx),
                OperatorArg::Number(ury),
            ],
        );
    }
}

// ============================================================================
// FFI Functions - Color Operators
// ============================================================================

/// Set stroke colorspace (CS operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_CS(_ctx: Handle, proc: Handle, name: *const c_char) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        let cs_name = if !name.is_null() {
            unsafe { CStr::from_ptr(name) }
                .to_str()
                .unwrap_or("DeviceGray")
                .to_string()
        } else {
            "DeviceGray".to_string()
        };
        proc_guard.gstate.stroke_cs = cs_name.clone();
        proc_guard.record_op("CS", vec![OperatorArg::Name(cs_name)]);
    }
}

/// Set fill colorspace (cs operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_cs(_ctx: Handle, proc: Handle, name: *const c_char) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        let cs_name = if !name.is_null() {
            unsafe { CStr::from_ptr(name) }
                .to_str()
                .unwrap_or("DeviceGray")
                .to_string()
        } else {
            "DeviceGray".to_string()
        };
        proc_guard.gstate.fill_cs = cs_name.clone();
        proc_guard.record_op("cs", vec![OperatorArg::Name(cs_name)]);
    }
}

/// Set stroke color (SC operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_SC_color(_ctx: Handle, proc: Handle, n: i32, color: *const f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        let colors = if !color.is_null() && n > 0 {
            unsafe { std::slice::from_raw_parts(color, n as usize) }.to_vec()
        } else {
            vec![0.0]
        };
        proc_guard.gstate.stroke_color = colors.clone();
        let args: Vec<OperatorArg> = colors.iter().map(|&c| OperatorArg::Number(c)).collect();
        proc_guard.record_op("SC", args);
    }
}

/// Set fill color (sc operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_sc_color(_ctx: Handle, proc: Handle, n: i32, color: *const f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        let colors = if !color.is_null() && n > 0 {
            unsafe { std::slice::from_raw_parts(color, n as usize) }.to_vec()
        } else {
            vec![0.0]
        };
        proc_guard.gstate.fill_color = colors.clone();
        let args: Vec<OperatorArg> = colors.iter().map(|&c| OperatorArg::Number(c)).collect();
        proc_guard.record_op("sc", args);
    }
}

/// Set stroke gray (G operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_G(_ctx: Handle, proc: Handle, g: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.stroke_cs = "DeviceGray".to_string();
        proc_guard.gstate.stroke_color = vec![g];
        proc_guard.record_op("G", vec![OperatorArg::Number(g)]);
    }
}

/// Set fill gray (g operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_g(_ctx: Handle, proc: Handle, g: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.fill_cs = "DeviceGray".to_string();
        proc_guard.gstate.fill_color = vec![g];
        proc_guard.record_op("g", vec![OperatorArg::Number(g)]);
    }
}

/// Set stroke RGB (RG operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_RG(_ctx: Handle, proc: Handle, r: f32, g: f32, b: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.stroke_cs = "DeviceRGB".to_string();
        proc_guard.gstate.stroke_color = vec![r, g, b];
        proc_guard.record_op(
            "RG",
            vec![
                OperatorArg::Number(r),
                OperatorArg::Number(g),
                OperatorArg::Number(b),
            ],
        );
    }
}

/// Set fill RGB (rg operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_rg(_ctx: Handle, proc: Handle, r: f32, g: f32, b: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.fill_cs = "DeviceRGB".to_string();
        proc_guard.gstate.fill_color = vec![r, g, b];
        proc_guard.record_op(
            "rg",
            vec![
                OperatorArg::Number(r),
                OperatorArg::Number(g),
                OperatorArg::Number(b),
            ],
        );
    }
}

/// Set stroke CMYK (K operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_K(_ctx: Handle, proc: Handle, c: f32, m: f32, y: f32, k: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.stroke_cs = "DeviceCMYK".to_string();
        proc_guard.gstate.stroke_color = vec![c, m, y, k];
        proc_guard.record_op(
            "K",
            vec![
                OperatorArg::Number(c),
                OperatorArg::Number(m),
                OperatorArg::Number(y),
                OperatorArg::Number(k),
            ],
        );
    }
}

/// Set fill CMYK (k operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_k(_ctx: Handle, proc: Handle, c: f32, m: f32, y: f32, k: f32) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.fill_cs = "DeviceCMYK".to_string();
        proc_guard.gstate.fill_color = vec![c, m, y, k];
        proc_guard.record_op(
            "k",
            vec![
                OperatorArg::Number(c),
                OperatorArg::Number(m),
                OperatorArg::Number(y),
                OperatorArg::Number(k),
            ],
        );
    }
}

// ============================================================================
// FFI Functions - XObject/Image/Shading Operators
// ============================================================================

/// Inline image (BI operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_BI(_ctx: Handle, proc: Handle, image: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("BI", vec![OperatorArg::Integer(image as i32)]);
    }
}

/// Shading (sh operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_sh(_ctx: Handle, proc: Handle, name: *const c_char) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        let shade_name = if !name.is_null() {
            unsafe { CStr::from_ptr(name) }.to_str().unwrap_or("")
        } else {
            ""
        };
        proc_guard.record_op("sh", vec![OperatorArg::Name(shade_name.to_string())]);
    }
}

/// XObject image (Do operator for images)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_Do_image(_ctx: Handle, proc: Handle, name: *const c_char, image: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        let img_name = if !name.is_null() {
            unsafe { CStr::from_ptr(name) }.to_str().unwrap_or("")
        } else {
            ""
        };
        proc_guard.record_op(
            "Do",
            vec![
                OperatorArg::Name(img_name.to_string()),
                OperatorArg::Integer(image as i32),
            ],
        );
    }
}

/// XObject form (Do operator for forms)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_Do_form(_ctx: Handle, proc: Handle, name: *const c_char, form: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        let form_name = if !name.is_null() {
            unsafe { CStr::from_ptr(name) }.to_str().unwrap_or("")
        } else {
            ""
        };
        proc_guard.record_op(
            "Do",
            vec![
                OperatorArg::Name(form_name.to_string()),
                OperatorArg::Integer(form as i32),
            ],
        );
    }
}

// ============================================================================
// FFI Functions - Marked Content Operators
// ============================================================================

/// Marked content point (MP operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_MP(_ctx: Handle, proc: Handle, tag: *const c_char) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        let tag_str = if !tag.is_null() {
            unsafe { CStr::from_ptr(tag) }.to_str().unwrap_or("")
        } else {
            ""
        };
        proc_guard.record_op("MP", vec![OperatorArg::Name(tag_str.to_string())]);
    }
}

/// Marked content point with properties (DP operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_DP(_ctx: Handle, proc: Handle, tag: *const c_char) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        let tag_str = if !tag.is_null() {
            unsafe { CStr::from_ptr(tag) }.to_str().unwrap_or("")
        } else {
            ""
        };
        proc_guard.record_op("DP", vec![OperatorArg::Name(tag_str.to_string())]);
    }
}

/// Begin marked content (BMC operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_BMC(_ctx: Handle, proc: Handle, tag: *const c_char) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        let tag_str = if !tag.is_null() {
            unsafe { CStr::from_ptr(tag) }.to_str().unwrap_or("")
        } else {
            ""
        };
        proc_guard.record_op("BMC", vec![OperatorArg::Name(tag_str.to_string())]);
    }
}

/// Begin marked content with properties (BDC operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_BDC(_ctx: Handle, proc: Handle, tag: *const c_char) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        let tag_str = if !tag.is_null() {
            unsafe { CStr::from_ptr(tag) }.to_str().unwrap_or("")
        } else {
            ""
        };
        proc_guard.record_op("BDC", vec![OperatorArg::Name(tag_str.to_string())]);
    }
}

/// End marked content (EMC operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_EMC(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("EMC", vec![]);
    }
}

// ============================================================================
// FFI Functions - Compatibility Operators
// ============================================================================

/// Begin compatibility section (BX operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_BX(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("BX", vec![]);
    }
}

/// End compatibility section (EX operator)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_EX(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("EX", vec![]);
    }
}

// ============================================================================
// FFI Functions - End Operators
// ============================================================================

/// End of data (before finalize)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_EOD(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("EOD", vec![]);
    }
}

/// End of stream (finalize)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_op_END(_ctx: Handle, proc: Handle) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let mut proc_guard = proc_arc.lock().unwrap();
        proc_guard.record_op("END", vec![]);
    }
}

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

/// Get processor type
#[unsafe(no_mangle)]
pub extern "C" fn pdf_processor_get_type(_ctx: Handle, proc: Handle) -> i32 {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let proc_guard = proc_arc.lock().unwrap();
        proc_guard.proc_type as i32
    } else {
        0
    }
}

/// Get number of operators processed
#[unsafe(no_mangle)]
pub extern "C" fn pdf_processor_get_operator_count(_ctx: Handle, proc: Handle) -> i32 {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let proc_guard = proc_arc.lock().unwrap();
        proc_guard.operators.len() as i32
    } else {
        0
    }
}

/// Get gstate stack depth
#[unsafe(no_mangle)]
pub extern "C" fn pdf_processor_get_gstate_depth(_ctx: Handle, proc: Handle) -> i32 {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate_stack.len() as i32
    } else {
        0
    }
}

/// Check if processor is in text object
#[unsafe(no_mangle)]
pub extern "C" fn pdf_processor_in_text(_ctx: Handle, proc: Handle) -> i32 {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let proc_guard = proc_arc.lock().unwrap();
        if proc_guard.in_text { 1 } else { 0 }
    } else {
        0
    }
}

/// Get current line width
#[unsafe(no_mangle)]
pub extern "C" fn pdf_processor_get_line_width(_ctx: Handle, proc: Handle) -> f32 {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let proc_guard = proc_arc.lock().unwrap();
        proc_guard.gstate.line_width
    } else {
        1.0
    }
}

/// Get current CTM
#[unsafe(no_mangle)]
pub extern "C" fn pdf_processor_get_ctm(
    _ctx: Handle,
    proc: Handle,
    a: *mut f32,
    b: *mut f32,
    c: *mut f32,
    d: *mut f32,
    e: *mut f32,
    f: *mut f32,
) {
    if let Some(proc_arc) = PDF_PROCESSORS.get(proc) {
        let proc_guard = proc_arc.lock().unwrap();
        let ctm = &proc_guard.gstate.ctm;
        unsafe {
            if !a.is_null() {
                *a = ctm.a;
            }
            if !b.is_null() {
                *b = ctm.b;
            }
            if !c.is_null() {
                *c = ctm.c;
            }
            if !d.is_null() {
                *d = ctm.d;
            }
            if !e.is_null() {
                *e = ctm.e;
            }
            if !f.is_null() {
                *f = ctm.f;
            }
        }
    }
}

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

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

    #[test]
    fn test_processor_creation() {
        let ctx = 1;
        let proc = pdf_new_processor(ctx, 0);
        assert!(proc > 0);
        pdf_drop_processor(ctx, proc);
    }

    #[test]
    fn test_run_processor() {
        let ctx = 1;
        let proc = pdf_new_run_processor(ctx, 0, 0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0, ptr::null());
        assert!(proc > 0);

        assert_eq!(pdf_processor_get_type(ctx, proc), ProcessorType::Run as i32);

        pdf_drop_processor(ctx, proc);
    }

    #[test]
    fn test_buffer_processor() {
        let ctx = 1;
        let proc = pdf_new_buffer_processor(ctx, 0, 0, 1);
        assert!(proc > 0);
        assert_eq!(
            pdf_processor_get_type(ctx, proc),
            ProcessorType::Buffer as i32
        );
        pdf_drop_processor(ctx, proc);
    }

    #[test]
    fn test_gstate_stack() {
        let ctx = 1;
        let proc = pdf_new_processor(ctx, 0);

        // Initial depth should be 0
        assert_eq!(pdf_processor_get_gstate_depth(ctx, proc), 0);

        // Push state
        pdf_op_q(ctx, proc);
        assert_eq!(pdf_processor_get_gstate_depth(ctx, proc), 1);

        // Push again
        pdf_op_q(ctx, proc);
        assert_eq!(pdf_processor_get_gstate_depth(ctx, proc), 2);

        // Pop
        pdf_op_Q(ctx, proc);
        assert_eq!(pdf_processor_get_gstate_depth(ctx, proc), 1);

        pdf_drop_processor(ctx, proc);
    }

    #[test]
    fn test_graphics_state_ops() {
        let ctx = 1;
        let proc = pdf_new_processor(ctx, 0);

        pdf_op_w(ctx, proc, 2.5);
        assert!((pdf_processor_get_line_width(ctx, proc) - 2.5).abs() < 0.001);

        pdf_op_j(ctx, proc, 1);
        pdf_op_J(ctx, proc, 2);
        pdf_op_M(ctx, proc, 5.0);
        pdf_op_i(ctx, proc, 0.5);

        // Check operator count
        assert_eq!(pdf_processor_get_operator_count(ctx, proc), 5);

        pdf_drop_processor(ctx, proc);
    }

    #[test]
    fn test_path_ops() {
        let ctx = 1;
        let proc = pdf_new_processor(ctx, 0);

        pdf_op_m(ctx, proc, 10.0, 20.0);
        pdf_op_l(ctx, proc, 100.0, 200.0);
        pdf_op_c(ctx, proc, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0);
        pdf_op_h(ctx, proc);
        pdf_op_S(ctx, proc);

        assert_eq!(pdf_processor_get_operator_count(ctx, proc), 5);

        pdf_drop_processor(ctx, proc);
    }

    #[test]
    fn test_text_ops() {
        let ctx = 1;
        let proc = pdf_new_processor(ctx, 0);

        pdf_op_BT(ctx, proc);
        assert_eq!(pdf_processor_in_text(ctx, proc), 1);

        let font = CString::new("F1").unwrap();
        pdf_op_Tf(ctx, proc, font.as_ptr(), 12.0);
        pdf_op_Td(ctx, proc, 100.0, 700.0);

        let text = CString::new("Hello").unwrap();
        pdf_op_Tj(ctx, proc, text.as_ptr(), 5);

        pdf_op_ET(ctx, proc);
        assert_eq!(pdf_processor_in_text(ctx, proc), 0);

        pdf_drop_processor(ctx, proc);
    }

    #[test]
    fn test_color_ops() {
        let ctx = 1;
        let proc = pdf_new_processor(ctx, 0);

        pdf_op_g(ctx, proc, 0.5);
        pdf_op_G(ctx, proc, 0.7);
        pdf_op_rg(ctx, proc, 1.0, 0.0, 0.0);
        pdf_op_RG(ctx, proc, 0.0, 1.0, 0.0);
        pdf_op_k(ctx, proc, 0.0, 1.0, 1.0, 0.0);
        pdf_op_K(ctx, proc, 1.0, 0.0, 0.0, 0.0);

        assert_eq!(pdf_processor_get_operator_count(ctx, proc), 6);

        pdf_drop_processor(ctx, proc);
    }

    #[test]
    fn test_ctm() {
        let ctx = 1;
        let proc = pdf_new_processor(ctx, 0);

        // Initial CTM should be identity
        let mut a: f32 = 0.0;
        let mut b: f32 = 0.0;
        let mut c: f32 = 0.0;
        let mut d: f32 = 0.0;
        let mut e: f32 = 0.0;
        let mut f: f32 = 0.0;
        pdf_processor_get_ctm(ctx, proc, &mut a, &mut b, &mut c, &mut d, &mut e, &mut f);

        assert!((a - 1.0).abs() < 0.001);
        assert!((d - 1.0).abs() < 0.001);

        // Apply translation
        pdf_op_cm(ctx, proc, 1.0, 0.0, 0.0, 1.0, 100.0, 200.0);

        pdf_processor_get_ctm(ctx, proc, &mut a, &mut b, &mut c, &mut d, &mut e, &mut f);
        assert!((e - 100.0).abs() < 0.001);
        assert!((f - 200.0).abs() < 0.001);

        pdf_drop_processor(ctx, proc);
    }

    #[test]
    fn test_marked_content() {
        let ctx = 1;
        let proc = pdf_new_processor(ctx, 0);

        let tag = CString::new("Span").unwrap();
        pdf_op_BMC(ctx, proc, tag.as_ptr());
        pdf_op_EMC(ctx, proc);

        assert_eq!(pdf_processor_get_operator_count(ctx, proc), 2);

        pdf_drop_processor(ctx, proc);
    }

    #[test]
    fn test_sanitize_filter() {
        let ctx = 1;

        // Create a buffer processor as the chain
        let buffer_proc = pdf_new_buffer_processor(ctx, 0, 0, 1);

        // Create sanitize filter
        let sanitize =
            pdf_new_sanitize_filter(ctx, 0, buffer_proc, 0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
        assert!(sanitize > 0);
        assert_eq!(
            pdf_processor_get_type(ctx, sanitize),
            ProcessorType::Sanitize as i32
        );

        pdf_drop_processor(ctx, sanitize);
    }

    #[test]
    fn test_resource_stack() {
        let ctx = 1;
        let proc = pdf_new_processor(ctx, 0);

        pdf_processor_push_resources(ctx, proc, 100);
        pdf_processor_push_resources(ctx, proc, 200);

        let popped1 = pdf_processor_pop_resources(ctx, proc);
        assert_eq!(popped1, 200);

        let popped2 = pdf_processor_pop_resources(ctx, proc);
        assert_eq!(popped2, 100);

        pdf_drop_processor(ctx, proc);
    }

    #[test]
    fn test_processor_close_reset() {
        let ctx = 1;
        let proc = pdf_new_processor(ctx, 0);

        // Add some state
        pdf_op_q(ctx, proc);
        pdf_op_w(ctx, proc, 5.0);

        // Close
        pdf_close_processor(ctx, proc);

        // Reset
        pdf_reset_processor(ctx, proc);

        // State should be reset
        assert_eq!(pdf_processor_get_gstate_depth(ctx, proc), 0);
        assert!((pdf_processor_get_line_width(ctx, proc) - 1.0).abs() < 0.001);

        pdf_drop_processor(ctx, proc);
    }

    #[test]
    fn test_keep_drop() {
        let ctx = 1;
        let proc = pdf_new_processor(ctx, 0);

        // Keep should increment ref count
        let kept = pdf_keep_processor(ctx, proc);
        assert_eq!(kept, proc);

        // First drop decrements
        pdf_drop_processor(ctx, proc);

        // Processor should still exist
        assert_eq!(
            pdf_processor_get_type(ctx, proc),
            ProcessorType::Base as i32
        );

        // Second drop should actually free
        pdf_drop_processor(ctx, proc);
    }

    // ================================================================
    // Tests for content stream tokenizer
    // ================================================================

    #[test]
    fn test_tokenize_basic_operators() {
        let data = b"1 0 0 1 100 200 cm";
        let tokens = tokenize_content_stream(data);
        // Should produce 6 number tokens + 1 keyword
        assert_eq!(tokens.len(), 7);
        match &tokens[6] {
            CsToken::Keyword(kw) => assert_eq!(kw, "cm"),
            _ => panic!("Expected keyword 'cm'"),
        }
    }

    #[test]
    fn test_tokenize_string_literal() {
        let data = b"(Hello World) Tj";
        let tokens = tokenize_content_stream(data);
        assert_eq!(tokens.len(), 2);
        match &tokens[0] {
            CsToken::StringLiteral(s) => assert_eq!(s, "Hello World"),
            _ => panic!("Expected string literal"),
        }
    }

    #[test]
    fn test_tokenize_nested_parens() {
        let data = b"(Hello (nested) World) Tj";
        let tokens = tokenize_content_stream(data);
        assert_eq!(tokens.len(), 2);
        match &tokens[0] {
            CsToken::StringLiteral(s) => assert_eq!(s, "Hello (nested) World"),
            _ => panic!("Expected string literal with nested parens"),
        }
    }

    #[test]
    fn test_tokenize_escape_sequences() {
        let data = b"(line1\\nline2) Tj";
        let tokens = tokenize_content_stream(data);
        assert_eq!(tokens.len(), 2);
        match &tokens[0] {
            CsToken::StringLiteral(s) => assert_eq!(s, "line1\nline2"),
            _ => panic!("Expected string with escape"),
        }
    }

    #[test]
    fn test_tokenize_hex_string() {
        let data = b"<48656C6C6F> Tj";
        let tokens = tokenize_content_stream(data);
        assert_eq!(tokens.len(), 2);
        match &tokens[0] {
            CsToken::HexString(s) => assert_eq!(s, "Hello"),
            _ => panic!("Expected hex string"),
        }
    }

    #[test]
    fn test_tokenize_name_object() {
        let data = b"/F1 12 Tf";
        let tokens = tokenize_content_stream(data);
        assert_eq!(tokens.len(), 3);
        match &tokens[0] {
            CsToken::Name(n) => assert_eq!(n, "F1"),
            _ => panic!("Expected name object"),
        }
    }

    #[test]
    fn test_tokenize_negative_numbers() {
        let data = b"-10 -20.5 m";
        let tokens = tokenize_content_stream(data);
        assert_eq!(tokens.len(), 3);
        match &tokens[0] {
            CsToken::Integer(n) => assert_eq!(*n, -10),
            _ => panic!("Expected integer -10"),
        }
        match &tokens[1] {
            CsToken::Number(n) => assert!((*n - (-20.5)).abs() < 0.001),
            _ => panic!("Expected real -20.5"),
        }
    }

    #[test]
    fn test_tokenize_comments() {
        let data = b"% this is a comment\n1 0 0 1 0 0 cm";
        let tokens = tokenize_content_stream(data);
        // Comment should be skipped, remaining: 6 numbers + cm
        assert_eq!(tokens.len(), 7);
    }

    #[test]
    fn test_tokens_to_operators_simple() {
        let data = b"q 1 0 0 1 100 200 cm Q";
        let tokens = tokenize_content_stream(data);
        let ops = tokens_to_operators(&tokens);
        assert_eq!(ops.len(), 3);
        assert_eq!(ops[0].0, "q");
        assert_eq!(ops[0].1.len(), 0);
        assert_eq!(ops[1].0, "cm");
        assert_eq!(ops[1].1.len(), 6);
        assert_eq!(ops[2].0, "Q");
    }

    #[test]
    fn test_tokens_to_operators_color() {
        let data = b"1 0 0 rg 0.5 G";
        let tokens = tokenize_content_stream(data);
        let ops = tokens_to_operators(&tokens);
        assert_eq!(ops.len(), 2);
        assert_eq!(ops[0].0, "rg");
        assert_eq!(ops[0].1.len(), 3);
        assert_eq!(ops[1].0, "G");
        assert_eq!(ops[1].1.len(), 1);
    }

    // ================================================================
    // Tests for pdf_process_contents
    // ================================================================

    #[test]
    fn test_process_contents_with_stream() {
        let ctx = 1;

        // Create a stream with content
        let content = b"q 1 0 0 1 72 720 cm 0.5 g 100 200 300 400 re f Q";
        let stm = STREAMS.insert(crate::ffi::stream::Stream::from_memory(content.to_vec()));

        // Create processor
        let proc = pdf_new_processor(ctx, 0);

        // Process contents
        let mut out_res: Handle = 0;
        pdf_process_contents(ctx, proc, 0, 42, stm, &mut out_res);

        // out_res should be the resource handle we passed in
        assert_eq!(out_res, 42);

        // Processor should have recorded operators:
        // push_gstate (from process_contents) + q, cm, g, re, f, Q + pop_gstate
        // The function does push_gstate/pop_gstate around the stream
        // and the stream itself contains q/Q, so we should see the
        // operators from the stream.
        let op_count = pdf_processor_get_operator_count(ctx, proc);
        assert!(
            op_count >= 6,
            "Expected at least 6 operators, got {}",
            op_count
        );

        // Graphics state should be restored (depth 0)
        assert_eq!(pdf_processor_get_gstate_depth(ctx, proc), 0);

        pdf_drop_processor(ctx, proc);
        STREAMS.remove(stm);
    }

    #[test]
    fn test_process_contents_empty_stream() {
        let ctx = 1;
        let stm = STREAMS.insert(crate::ffi::stream::Stream::from_memory(Vec::new()));
        let proc = pdf_new_processor(ctx, 0);

        let mut out_res: Handle = 0;
        pdf_process_contents(ctx, proc, 0, 10, stm, &mut out_res);

        // No operators from the empty stream
        assert_eq!(pdf_processor_get_operator_count(ctx, proc), 0);
        assert_eq!(out_res, 10);

        pdf_drop_processor(ctx, proc);
        STREAMS.remove(stm);
    }

    #[test]
    fn test_process_contents_state_isolation() {
        let ctx = 1;

        // Stream sets line width to 5
        let content = b"5 w";
        let stm = STREAMS.insert(crate::ffi::stream::Stream::from_memory(content.to_vec()));

        let proc = pdf_new_processor(ctx, 0);

        // Set initial line width to 2
        pdf_op_w(ctx, proc, 2.0);

        pdf_process_contents(ctx, proc, 0, 0, stm, ptr::null_mut());

        // Line width should be restored to 2 because
        // process_contents saves/restores gstate
        assert!(
            (pdf_processor_get_line_width(ctx, proc) - 2.0).abs() < 0.001,
            "Expected line width 2.0, got {}",
            pdf_processor_get_line_width(ctx, proc)
        );

        pdf_drop_processor(ctx, proc);
        STREAMS.remove(stm);
    }

    // ================================================================
    // Tests for pdf_process_raw_contents
    // ================================================================

    #[test]
    fn test_process_raw_contents() {
        let ctx = 1;

        let content = b"q 1 0 0 1 0 0 cm Q";
        let stm = STREAMS.insert(crate::ffi::stream::Stream::from_memory(content.to_vec()));

        let proc = pdf_new_processor(ctx, 0);

        pdf_process_raw_contents(ctx, proc, 0, stm);

        // Should record q, cm, Q = 3 operators
        assert_eq!(pdf_processor_get_operator_count(ctx, proc), 3);

        pdf_drop_processor(ctx, proc);
        STREAMS.remove(stm);
    }

    #[test]
    fn test_process_raw_contents_no_state_isolation() {
        let ctx = 1;

        // Stream modifies line width but does not save/restore
        let content = b"5 w";
        let stm = STREAMS.insert(crate::ffi::stream::Stream::from_memory(content.to_vec()));

        let proc = pdf_new_processor(ctx, 0);
        pdf_op_w(ctx, proc, 2.0);

        pdf_process_raw_contents(ctx, proc, 0, stm);

        // Raw processing does NOT save/restore state, so line width
        // should be changed to 5
        assert!(
            (pdf_processor_get_line_width(ctx, proc) - 5.0).abs() < 0.001,
            "Expected line width 5.0 from raw processing, got {}",
            pdf_processor_get_line_width(ctx, proc)
        );

        pdf_drop_processor(ctx, proc);
        STREAMS.remove(stm);
    }

    // ================================================================
    // Tests for pdf_process_annot
    // ================================================================

    #[test]
    fn test_process_annot_square() {
        use crate::ffi::annot::ANNOTATIONS;

        let ctx = 1;

        // Create a square annotation with color
        let mut annot_obj = crate::pdf::annot::Annotation::new(
            crate::pdf::annot::AnnotType::Square,
            Rect::new(10.0, 20.0, 110.0, 70.0),
        );
        annot_obj.set_color(Some([1.0, 0.0, 0.0]));
        let annot = ANNOTATIONS.insert(annot_obj);

        let proc = pdf_new_processor(ctx, 0);

        pdf_process_annot(ctx, proc, annot);

        // Should have: annot metadata + RG + re + S
        let count = pdf_processor_get_operator_count(ctx, proc);
        assert!(
            count >= 3,
            "Expected at least 3 operators for Square annot, got {}",
            count
        );

        // Graphics state should be balanced (push/pop around annot)
        assert_eq!(pdf_processor_get_gstate_depth(ctx, proc), 0);

        pdf_drop_processor(ctx, proc);
        ANNOTATIONS.remove(annot);
    }

    #[test]
    fn test_process_annot_highlight() {
        use crate::ffi::annot::ANNOTATIONS;

        let ctx = 1;

        let annot_obj = crate::pdf::annot::Annotation::highlight(
            Rect::new(50.0, 100.0, 200.0, 120.0),
            [1.0, 1.0, 0.0],
        );
        let annot = ANNOTATIONS.insert(annot_obj);

        let proc = pdf_new_processor(ctx, 0);
        pdf_process_annot(ctx, proc, annot);

        // Should have: annot + rg + re + f
        let count = pdf_processor_get_operator_count(ctx, proc);
        assert!(
            count >= 3,
            "Expected at least 3 operators for Highlight annot, got {}",
            count
        );

        pdf_drop_processor(ctx, proc);
        ANNOTATIONS.remove(annot);
    }

    #[test]
    fn test_process_annot_missing() {
        let ctx = 1;
        let proc = pdf_new_processor(ctx, 0);

        // Process a nonexistent annotation handle
        pdf_process_annot(ctx, proc, 999999);

        // Should still record the bare "annot" marker
        let count = pdf_processor_get_operator_count(ctx, proc);
        assert_eq!(count, 1, "Expected 1 marker operator for missing annot");

        pdf_drop_processor(ctx, proc);
    }

    // ================================================================
    // Tests for pdf_process_glyph
    // ================================================================

    #[test]
    fn test_process_glyph() {
        let ctx = 1;

        // Create a simple glyph stream: set width and draw a rectangle
        let glyph_data = b"500 0 d0 0 0 500 700 re f";
        let res = STREAMS.insert(crate::ffi::stream::Stream::from_memory(glyph_data.to_vec()));

        let proc = pdf_new_processor(ctx, 0);
        pdf_process_glyph(ctx, proc, 0, res);

        // Should have d0, re, f = 3 operators
        let count = pdf_processor_get_operator_count(ctx, proc);
        assert_eq!(
            count, 3,
            "Expected 3 operators from glyph stream, got {}",
            count
        );

        // State should be balanced
        assert_eq!(pdf_processor_get_gstate_depth(ctx, proc), 0);

        pdf_drop_processor(ctx, proc);
        STREAMS.remove(res);
    }

    #[test]
    fn test_process_glyph_empty() {
        let ctx = 1;

        let res = STREAMS.insert(crate::ffi::stream::Stream::from_memory(Vec::new()));

        let proc = pdf_new_processor(ctx, 0);
        pdf_process_glyph(ctx, proc, 0, res);

        assert_eq!(pdf_processor_get_operator_count(ctx, proc), 0);
        assert_eq!(pdf_processor_get_gstate_depth(ctx, proc), 0);

        pdf_drop_processor(ctx, proc);
        STREAMS.remove(res);
    }

    // ================================================================
    // Tests for pdf_count_q_balance
    // ================================================================

    #[test]
    fn test_q_balance_balanced() {
        let content = b"q 1 0 0 1 0 0 cm Q";
        let stm = STREAMS.insert(crate::ffi::stream::Stream::from_memory(content.to_vec()));

        let mut prepend: i32 = -1;
        let mut append: i32 = -1;
        pdf_count_q_balance(0, 0, 0, stm, &mut prepend, &mut append);

        assert_eq!(prepend, 0, "Balanced stream needs no prepend");
        assert_eq!(append, 0, "Balanced stream needs no append");

        STREAMS.remove(stm);
    }

    #[test]
    #[allow(non_snake_case)]
    fn test_q_balance_missing_Q() {
        // Two q's but only one Q
        let content = b"q q 1 0 0 1 0 0 cm Q";
        let stm = STREAMS.insert(crate::ffi::stream::Stream::from_memory(content.to_vec()));

        let mut prepend: i32 = -1;
        let mut append: i32 = -1;
        pdf_count_q_balance(0, 0, 0, stm, &mut prepend, &mut append);

        assert_eq!(prepend, 0, "No orphan Q's");
        assert_eq!(append, 1, "One unclosed q needs one Q appended");

        STREAMS.remove(stm);
    }

    #[test]
    fn test_q_balance_missing_q() {
        // One Q without a matching q
        let content = b"Q";
        let stm = STREAMS.insert(crate::ffi::stream::Stream::from_memory(content.to_vec()));

        let mut prepend: i32 = -1;
        let mut append: i32 = -1;
        pdf_count_q_balance(0, 0, 0, stm, &mut prepend, &mut append);

        assert_eq!(prepend, 1, "One orphan Q needs one q prepended");
        assert_eq!(append, 0, "No unclosed q's");

        STREAMS.remove(stm);
    }

    #[test]
    fn test_q_balance_complex() {
        // Q Q q q q Q  →  2 orphan Q's at start, then q q q Q leaves depth 2
        let content = b"Q Q q q q Q";
        let stm = STREAMS.insert(crate::ffi::stream::Stream::from_memory(content.to_vec()));

        let mut prepend: i32 = -1;
        let mut append: i32 = -1;
        pdf_count_q_balance(0, 0, 0, stm, &mut prepend, &mut append);

        assert_eq!(prepend, 2, "Two orphan Q's need two q's prepended");
        assert_eq!(append, 2, "Two unclosed q's need two Q's appended");

        STREAMS.remove(stm);
    }

    #[test]
    fn test_q_balance_empty() {
        let stm = STREAMS.insert(crate::ffi::stream::Stream::from_memory(Vec::new()));

        let mut prepend: i32 = -1;
        let mut append: i32 = -1;
        pdf_count_q_balance(0, 0, 0, stm, &mut prepend, &mut append);

        assert_eq!(prepend, 0);
        assert_eq!(append, 0);

        STREAMS.remove(stm);
    }

    #[test]
    fn test_q_balance_null_outputs() {
        let content = b"q q Q";
        let stm = STREAMS.insert(crate::ffi::stream::Stream::from_memory(content.to_vec()));

        // Should not crash with null pointers
        pdf_count_q_balance(0, 0, 0, stm, ptr::null_mut(), ptr::null_mut());

        STREAMS.remove(stm);
    }

    // ================================================================
    // Tests for content stream operator dispatch
    // ================================================================

    #[test]
    fn test_process_contents_text_operators() {
        let ctx = 1;

        let content = b"BT /F1 12 Tf 100 700 Td (Hello World) Tj ET";
        let stm = STREAMS.insert(crate::ffi::stream::Stream::from_memory(content.to_vec()));

        let proc = pdf_new_processor(ctx, 0);
        pdf_process_contents(ctx, proc, 0, 0, stm, ptr::null_mut());

        // Should have BT, Tf, Td, Tj, ET = 5 operators
        let count = pdf_processor_get_operator_count(ctx, proc);
        assert_eq!(count, 5, "Expected 5 text operators, got {}", count);

        // After processing, text object should be closed
        assert_eq!(pdf_processor_in_text(ctx, proc), 0);

        pdf_drop_processor(ctx, proc);
        STREAMS.remove(stm);
    }

    #[test]
    fn test_process_contents_path_operators() {
        let ctx = 1;

        let content = b"10 20 m 100 200 l 30 40 50 60 70 80 c h S";
        let stm = STREAMS.insert(crate::ffi::stream::Stream::from_memory(content.to_vec()));

        let proc = pdf_new_processor(ctx, 0);
        pdf_process_contents(ctx, proc, 0, 0, stm, ptr::null_mut());

        // m, l, c, h, S = 5 operators
        let count = pdf_processor_get_operator_count(ctx, proc);
        assert_eq!(count, 5, "Expected 5 path operators, got {}", count);

        pdf_drop_processor(ctx, proc);
        STREAMS.remove(stm);
    }

    #[test]
    fn test_process_contents_color_operators() {
        let ctx = 1;

        let content = b"1 0 0 rg 0 1 0 RG 0.5 g 0.7 G 1 0 0 0 k 0 1 0 0 K";
        let stm = STREAMS.insert(crate::ffi::stream::Stream::from_memory(content.to_vec()));

        let proc = pdf_new_processor(ctx, 0);
        pdf_process_contents(ctx, proc, 0, 0, stm, ptr::null_mut());

        // rg, RG, g, G, k, K = 6 operators
        let count = pdf_processor_get_operator_count(ctx, proc);
        assert_eq!(count, 6, "Expected 6 color operators, got {}", count);

        pdf_drop_processor(ctx, proc);
        STREAMS.remove(stm);
    }

    #[test]
    fn test_process_contents_with_buffer() {
        let ctx = 1;

        // Test that BUFFERS store also works as a data source
        let content = b"q Q";
        let buf = BUFFERS.insert(crate::ffi::buffer::Buffer::from_data(content));

        let proc = pdf_new_processor(ctx, 0);
        pdf_process_contents(ctx, proc, 0, 0, buf, ptr::null_mut());

        // q, Q = 2 operators
        assert_eq!(pdf_processor_get_operator_count(ctx, proc), 2);

        pdf_drop_processor(ctx, proc);
        BUFFERS.remove(buf);
    }

    #[test]
    fn test_apply_operator_star_variants() {
        let ctx = 1;

        // Test f*, B*, b*, W* operators
        let content = b"0 0 100 100 re W* f* 0 0 50 50 re B* 0 0 25 25 re b*";
        let stm = STREAMS.insert(crate::ffi::stream::Stream::from_memory(content.to_vec()));

        let proc = pdf_new_processor(ctx, 0);
        pdf_process_raw_contents(ctx, proc, 0, stm);

        // re, W*, f*, re, B*, re, b* = 7 operators
        let count = pdf_processor_get_operator_count(ctx, proc);
        assert_eq!(count, 7, "Expected 7 operators, got {}", count);

        pdf_drop_processor(ctx, proc);
        STREAMS.remove(stm);
    }
}
