//! PDF Page FFI Module
//!
//! Provides page loading, manipulation, and rendering capabilities for PDF documents.
//! This module implements the MicroPDF pdf_page API for handling PDF pages.

use crate::ffi::annot::ANNOTATIONS;
use crate::ffi::color::{DEFAULT_COLORSPACES, DefaultColorspaces};
use crate::ffi::device::DEVICES;
use crate::ffi::link::LINKS;
use crate::ffi::pdf_object::types::{PDF_OBJECTS, PdfObj, PdfObjType};
use crate::ffi::separation::{SEPARATIONS, Separation, SeparationBehavior, Separations};
use crate::ffi::transition::{Transition, TransitionType};
use crate::ffi::{DOCUMENTS, Handle, HandleStore, PIXMAPS};
use crate::fitz::colorspace::Colorspace;
use crate::fitz::device::Device;
use crate::fitz::font::Font;
use crate::fitz::geometry::{Matrix, Rect};
use crate::fitz::link::Link as FitzLink;
use crate::fitz::text::{Text, TextItem, TextSpan};
use crate::pdf::annot::{AnnotType, Annotation};
use std::collections::HashMap;
use std::ffi::{CStr, CString, c_char, c_void};
use std::ptr;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::{Arc, LazyLock, Mutex};

// ============================================================================
// Type Aliases for Handles
// ============================================================================

pub type ContextHandle = Handle;
pub type DocumentHandle = Handle;
pub type PageHandle = Handle;
pub type DeviceHandle = Handle;
pub type CookieHandle = Handle;
pub type ColorspaceHandle = Handle;
pub type SeparationsHandle = Handle;
pub type LinkHandle = Handle;
pub type AnnotHandle = Handle;
pub type PdfObjHandle = Handle;
pub type PixmapHandle = Handle;
pub type TransitionHandle = Handle;
pub type DefaultColorspacesHandle = Handle;

// ============================================================================
// Box Types
// ============================================================================

/// PDF page box types
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(C)]
pub enum BoxType {
    #[default]
    MediaBox = 0,
    CropBox = 1,
    BleedBox = 2,
    TrimBox = 3,
    ArtBox = 4,
    UnknownBox = 5,
}

impl BoxType {
    pub fn from_i32(value: i32) -> Self {
        match value {
            0 => BoxType::MediaBox,
            1 => BoxType::CropBox,
            2 => BoxType::BleedBox,
            3 => BoxType::TrimBox,
            4 => BoxType::ArtBox,
            _ => BoxType::UnknownBox,
        }
    }

    pub fn from_string(name: &str) -> Self {
        match name.to_lowercase().as_str() {
            "mediabox" | "media" => BoxType::MediaBox,
            "cropbox" | "crop" => BoxType::CropBox,
            "bleedbox" | "bleed" => BoxType::BleedBox,
            "trimbox" | "trim" => BoxType::TrimBox,
            "artbox" | "art" => BoxType::ArtBox,
            _ => BoxType::UnknownBox,
        }
    }

    pub fn to_string(&self) -> &'static str {
        match self {
            BoxType::MediaBox => "MediaBox",
            BoxType::CropBox => "CropBox",
            BoxType::BleedBox => "BleedBox",
            BoxType::TrimBox => "TrimBox",
            BoxType::ArtBox => "ArtBox",
            BoxType::UnknownBox => "Unknown",
        }
    }
}

// ============================================================================
// Redaction Options
// ============================================================================

/// Image redaction methods
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(C)]
pub enum RedactImageMethod {
    #[default]
    None = 0,
    Remove = 1,
    Pixels = 2,
    RemoveUnlessInvisible = 3,
}

/// Line art redaction methods
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(C)]
pub enum RedactLineArtMethod {
    #[default]
    None = 0,
    RemoveIfCovered = 1,
    RemoveIfTouched = 2,
}

/// Text redaction methods
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(C)]
pub enum RedactTextMethod {
    #[default]
    Remove = 0,
    None = 1,
    RemoveInvisible = 2,
}

/// Redaction options structure
#[derive(Debug, Clone, Default)]
#[repr(C)]
pub struct RedactOptions {
    pub black_boxes: i32,
    pub image_method: RedactImageMethod,
    pub line_art: RedactLineArtMethod,
    pub text: RedactTextMethod,
}

// ============================================================================
// Page Structure
// ============================================================================

/// Link structure
#[derive(Debug, Clone)]
pub struct Link {
    pub rect: Rect,
    pub uri: String,
    pub next: Option<Box<Link>>,
}

/// Annotation reference
#[derive(Debug, Clone)]
pub struct AnnotRef {
    pub handle: AnnotHandle,
    pub subtype: String,
    pub rect: Rect,
}

/// PDF Page structure
#[derive(Debug)]
pub struct PdfPage {
    pub refs: i32,
    pub doc: DocumentHandle,
    pub obj: PdfObjHandle,
    pub chapter: i32,
    pub number: i32,
    pub incomplete: bool,
    pub in_doc: bool,
    pub transparency: bool,
    pub overprint: bool,

    // Page boxes (cached)
    pub media_box: Rect,
    pub crop_box: Option<Rect>,
    pub bleed_box: Option<Rect>,
    pub trim_box: Option<Rect>,
    pub art_box: Option<Rect>,

    // Rotation (0, 90, 180, 270)
    pub rotation: i32,

    // Resources and contents handles
    pub resources: PdfObjHandle,
    pub contents: PdfObjHandle,
    pub group: PdfObjHandle,

    // Links
    pub links: Vec<Link>,

    // Annotations and widgets
    pub annots: Vec<AnnotRef>,
    pub widgets: Vec<AnnotRef>,

    // User unit (PDF 1.6+)
    pub user_unit: f32,
}

impl Default for PdfPage {
    fn default() -> Self {
        Self {
            refs: 1,
            doc: 0,
            obj: 0,
            chapter: 0,
            number: 0,
            incomplete: false,
            in_doc: false,
            transparency: false,
            overprint: false,
            media_box: Rect::new(0.0, 0.0, 612.0, 792.0), // Default US Letter
            crop_box: None,
            bleed_box: None,
            trim_box: None,
            art_box: None,
            rotation: 0,
            resources: 0,
            contents: 0,
            group: 0,
            links: Vec::new(),
            annots: Vec::new(),
            widgets: Vec::new(),
            user_unit: 1.0,
        }
    }
}

impl PdfPage {
    pub fn new(doc: DocumentHandle, number: i32) -> Self {
        Self {
            doc,
            number,
            ..Default::default()
        }
    }

    /// Get the effective crop box (crop box or media box if not set)
    pub fn get_crop_box(&self) -> Rect {
        self.crop_box.unwrap_or(self.media_box)
    }

    /// Get the specified box type
    pub fn get_box(&self, box_type: BoxType) -> Rect {
        match box_type {
            BoxType::MediaBox => self.media_box,
            BoxType::CropBox => self.get_crop_box(),
            BoxType::BleedBox => self.bleed_box.unwrap_or_else(|| self.get_crop_box()),
            BoxType::TrimBox => self.trim_box.unwrap_or_else(|| self.get_crop_box()),
            BoxType::ArtBox => self.art_box.unwrap_or_else(|| self.get_crop_box()),
            BoxType::UnknownBox => self.get_crop_box(),
        }
    }

    /// Get the transformation matrix for the page
    pub fn get_transform(&self, box_type: BoxType) -> Matrix {
        let rect = self.get_box(box_type);

        // Start with identity matrix
        let mut ctm = Matrix::IDENTITY;

        // Apply rotation
        match self.rotation {
            90 => {
                ctm = Matrix::rotate(90.0);
                ctm = ctm.concat(&Matrix::translate(rect.height(), 0.0));
            }
            180 => {
                ctm = Matrix::rotate(180.0);
                ctm = ctm.concat(&Matrix::translate(rect.width(), rect.height()));
            }
            270 => {
                ctm = Matrix::rotate(270.0);
                ctm = ctm.concat(&Matrix::translate(0.0, rect.width()));
            }
            _ => {}
        }

        // Apply user unit scaling
        if (self.user_unit - 1.0).abs() > f32::EPSILON {
            ctm = ctm.concat(&Matrix::scale(self.user_unit, self.user_unit));
        }

        // Translate to origin
        ctm = ctm.concat(&Matrix::translate(-rect.x0, -rect.y0));

        ctm
    }

    /// Get the bounds of the page after transformation
    pub fn get_bounds(&self, box_type: BoxType) -> Rect {
        let rect = self.get_box(box_type);
        let ctm = self.get_transform(box_type);

        // Transform all four corners and compute bounding box
        rect.transform(&ctm)
    }

    /// Check if page has transparency
    pub fn has_transparency(&self) -> bool {
        self.transparency
    }

    /// Set a page box
    pub fn set_box(&mut self, box_type: BoxType, rect: Rect) {
        match box_type {
            BoxType::MediaBox => self.media_box = rect,
            BoxType::CropBox => self.crop_box = Some(rect),
            BoxType::BleedBox => self.bleed_box = Some(rect),
            BoxType::TrimBox => self.trim_box = Some(rect),
            BoxType::ArtBox => self.art_box = Some(rect),
            BoxType::UnknownBox => {}
        }
    }
}

// ============================================================================
// PDF Data Parsing Helpers
// ============================================================================

/// Parse the /Count value from the root /Type /Pages dictionary in raw PDF bytes.
fn parse_page_count_from_pdf(data: &[u8]) -> Option<i32> {
    let pages_pattern = b"/Type /Pages";
    let pages_pattern_nospace = b"/Type/Pages";

    for pattern in &[pages_pattern.as_slice(), pages_pattern_nospace.as_slice()] {
        for i in 0..data.len().saturating_sub(pattern.len()) {
            if &data[i..i + pattern.len()] == *pattern {
                let search_start = i.saturating_sub(100);
                let search_end = (i + 300).min(data.len());
                let region = &data[search_start..search_end];

                if let Some(count) = extract_count_from_region(region) {
                    if count > 0 && count < 100_000 {
                        return Some(count);
                    }
                }
            }
        }
    }

    // Fallback: search for any /Count pattern
    let count_pattern = b"/Count ";
    for i in 0..data.len().saturating_sub(count_pattern.len() + 5) {
        if &data[i..i + count_pattern.len()] == count_pattern {
            let after = &data[i + count_pattern.len()..];
            if let Some(count) = parse_leading_int(after) {
                if count > 0 && count < 100_000 {
                    return Some(count);
                }
            }
        }
    }

    None
}

/// Extract /Count N from a byte region.
fn extract_count_from_region(data: &[u8]) -> Option<i32> {
    let pattern = b"/Count ";
    for i in 0..data.len().saturating_sub(pattern.len()) {
        if &data[i..i + pattern.len()] == pattern {
            return parse_leading_int(&data[i + pattern.len()..]);
        }
    }
    let pattern2 = b"/Count";
    for i in 0..data.len().saturating_sub(pattern2.len() + 1) {
        if &data[i..i + pattern2.len()] == pattern2 {
            let rest = &data[i + pattern2.len()..];
            let mut start = 0;
            while start < rest.len() && rest[start].is_ascii_whitespace() {
                start += 1;
            }
            if start < rest.len() && rest[start].is_ascii_digit() {
                return parse_leading_int(&rest[start..]);
            }
        }
    }
    None
}

/// Parse a leading integer from a byte slice.
fn parse_leading_int(data: &[u8]) -> Option<i32> {
    let mut end = 0;
    while end < data.len() && data[end].is_ascii_digit() {
        end += 1;
    }
    if end > 0 {
        std::str::from_utf8(&data[..end])
            .ok()
            .and_then(|s| s.parse::<i32>().ok())
    } else {
        None
    }
}

/// Find the N-th page object (0-based) in the PDF data.
/// Returns (object_number, object_byte_offset).
fn find_page_object_in_pdf(data: &[u8], page_index: i32) -> Option<(i32, usize)> {
    let page_pattern = b"/Type /Page";
    let page_pattern_nospace = b"/Type/Page";
    let mut pages_found: Vec<(i32, usize)> = Vec::new();

    for pattern in &[page_pattern.as_slice(), page_pattern_nospace.as_slice()] {
        for i in 0..data.len().saturating_sub(pattern.len()) {
            if &data[i..i + pattern.len()] == *pattern {
                let next_idx = i + pattern.len();
                if next_idx < data.len() && data[next_idx] == b's' {
                    continue;
                }
                if next_idx < data.len() && data[next_idx].is_ascii_alphabetic() {
                    continue;
                }

                if let Some(obj_num) = find_obj_number_before(data, i) {
                    let obj_start = find_obj_start(data, i, obj_num);
                    if !pages_found.iter().any(|(n, _)| *n == obj_num) {
                        pages_found.push((obj_num, obj_start));
                    }
                }
            }
        }
    }

    pages_found.sort_by_key(|&(_, pos)| pos);
    pages_found.get(page_index as usize).copied()
}

/// Find the object number before a given position (looks for "N M obj" pattern).
fn find_obj_number_before(data: &[u8], pos: usize) -> Option<i32> {
    let obj_pattern = b" obj";
    let search_start = pos.saturating_sub(500);
    let search_data = &data[search_start..pos];

    let mut last_obj_pos = None;
    for i in 0..search_data.len().saturating_sub(obj_pattern.len()) {
        if &search_data[i..i + obj_pattern.len()] == obj_pattern {
            last_obj_pos = Some(search_start + i);
        }
    }

    if let Some(obj_pos) = last_obj_pos {
        let before_obj = &data[obj_pos.saturating_sub(30)..obj_pos];

        let mut end = before_obj.len();
        while end > 0 && before_obj[end - 1].is_ascii_whitespace() {
            end -= 1;
        }
        while end > 0 && before_obj[end - 1].is_ascii_digit() {
            end -= 1;
        }
        while end > 0 && before_obj[end - 1].is_ascii_whitespace() {
            end -= 1;
        }
        let mut start = end;
        while start > 0 && before_obj[start - 1].is_ascii_digit() {
            start -= 1;
        }

        if start < end {
            let num_str = std::str::from_utf8(&before_obj[start..end]).ok()?;
            return num_str.parse::<i32>().ok();
        }
    }

    None
}

/// Find the byte offset where an object starts, given a position inside it.
fn find_obj_start(data: &[u8], pos_inside: usize, obj_num: i32) -> usize {
    let obj_marker = format!("{} 0 obj", obj_num);
    let marker_bytes = obj_marker.as_bytes();
    let search_start = pos_inside.saturating_sub(500);

    for i in (search_start..pos_inside).rev() {
        if i + marker_bytes.len() <= data.len() && &data[i..i + marker_bytes.len()] == marker_bytes
        {
            return i;
        }
    }

    pos_inside
}

/// Extract the object dictionary region for a given object.
fn extract_object_region(data: &[u8], obj_start: usize) -> &[u8] {
    let endobj_pattern = b"endobj";
    let search_end = (obj_start + 10000).min(data.len());
    for i in obj_start..search_end.saturating_sub(endobj_pattern.len()) {
        if &data[i..i + endobj_pattern.len()] == endobj_pattern {
            return &data[obj_start..i + endobj_pattern.len()];
        }
    }
    &data[obj_start..(obj_start + 2000).min(data.len())]
}

/// Parse a /MediaBox [x0 y0 x1 y1] from a byte region.
fn parse_mediabox_from_region(region: &[u8]) -> Option<Rect> {
    parse_box_from_region(region, b"/MediaBox")
}

/// Parse a named box from a byte region.
fn parse_box_from_region(region: &[u8], box_name: &[u8]) -> Option<Rect> {
    for i in 0..region.len().saturating_sub(box_name.len()) {
        if &region[i..i + box_name.len()] == box_name {
            let after = &region[i + box_name.len()..];
            return parse_rect_array(after);
        }
    }
    None
}

/// Parse a `[x0 y0 x1 y1]` array from bytes, skipping leading whitespace.
fn parse_rect_array(data: &[u8]) -> Option<Rect> {
    let mut pos = 0;
    while pos < data.len() && data[pos].is_ascii_whitespace() {
        pos += 1;
    }
    if pos >= data.len() || data[pos] != b'[' {
        return None;
    }
    pos += 1;

    let mut values = [0.0f32; 4];
    for value in &mut values {
        while pos < data.len() && data[pos].is_ascii_whitespace() {
            pos += 1;
        }
        let num_start = pos;
        if pos < data.len() && (data[pos] == b'-' || data[pos] == b'+') {
            pos += 1;
        }
        while pos < data.len() && (data[pos].is_ascii_digit() || data[pos] == b'.') {
            pos += 1;
        }
        if pos == num_start {
            return None;
        }
        let num_str = std::str::from_utf8(&data[num_start..pos]).ok()?;
        *value = num_str.parse::<f32>().ok()?;
    }

    Some(Rect::new(values[0], values[1], values[2], values[3]))
}

/// Parse /Rotate N from a byte region. Returns 0, 90, 180, or 270.
fn parse_rotate_from_region(region: &[u8]) -> i32 {
    let rotate_pattern = b"/Rotate";
    for i in 0..region.len().saturating_sub(rotate_pattern.len()) {
        if &region[i..i + rotate_pattern.len()] == rotate_pattern {
            let after = &region[i + rotate_pattern.len()..];
            let mut pos = 0;
            while pos < after.len() && after[pos].is_ascii_whitespace() {
                pos += 1;
            }
            let negative = pos < after.len() && after[pos] == b'-';
            if negative {
                pos += 1;
            }
            if let Some(val) = parse_leading_int(&after[pos..]) {
                let rotation = if negative { -val } else { val };
                return ((rotation % 360) + 360) % 360;
            }
        }
    }
    0
}

/// Parse /Dur N from a byte region (page display duration in seconds). Returns 0.0 if not found.
fn parse_dur_from_region(region: &[u8]) -> f32 {
    let pattern = b"/Dur";
    for i in 0..region.len().saturating_sub(pattern.len()) {
        if &region[i..i + pattern.len()] == pattern {
            let after = &region[i + pattern.len()..];
            let mut pos = 0;
            while pos < after.len() && after[pos].is_ascii_whitespace() {
                pos += 1;
            }
            let num_start = pos;
            if pos < after.len() && (after[pos] == b'-' || after[pos] == b'+') {
                pos += 1;
            }
            while pos < after.len() && (after[pos].is_ascii_digit() || after[pos] == b'.') {
                pos += 1;
            }
            if pos > num_start {
                if let Ok(val) = std::str::from_utf8(&after[num_start..pos])
                    .unwrap_or("0")
                    .parse::<f32>()
                {
                    return val;
                }
            }
        }
    }
    0.0
}

/// Parse /Trans << ... >> dictionary and extract transition type and duration.
/// Returns (TransitionType, duration, vertical, outwards, direction).
fn parse_trans_from_region(region: &[u8]) -> Option<(TransitionType, f32, i32, i32, i32)> {
    let trans_pattern = b"/Trans";
    let trans_start = region
        .windows(trans_pattern.len())
        .position(|w| w == trans_pattern)?;
    let after_trans = &region[trans_start + trans_pattern.len()..];

    let dict_start = after_trans.iter().position(|&b| b == b'<')?;
    if dict_start + 1 >= after_trans.len() || after_trans[dict_start + 1] != b'<' {
        return None;
    }
    let dict_region = &after_trans[dict_start..];

    let mut trans_type = TransitionType::None;
    let mut duration = 1.0f32;
    let mut vertical = 1i32;
    let mut outwards = 0i32;
    let mut direction = 0i32;

    // Parse /S (transition subtype)
    let s_pattern = b"/S";
    for i in 0..dict_region.len().saturating_sub(s_pattern.len() + 2) {
        if &dict_region[i..i + s_pattern.len()] == s_pattern {
            let after = &dict_region[i + s_pattern.len()..];
            let mut pos = 0;
            while pos < after.len() && after[pos].is_ascii_whitespace() {
                pos += 1;
            }
            if pos < after.len() && after[pos] == b'/' {
                pos += 1;
                let name_start = pos;
                while pos < after.len()
                    && !after[pos].is_ascii_whitespace()
                    && after[pos] != b'/'
                    && after[pos] != b'>'
                {
                    pos += 1;
                }
                if pos > name_start {
                    let name = std::str::from_utf8(&after[name_start..pos]).unwrap_or("");
                    trans_type = match name {
                        "Split" => TransitionType::Split,
                        "Blinds" => TransitionType::Blinds,
                        "Box" => TransitionType::Box,
                        "Wipe" => TransitionType::Wipe,
                        "Dissolve" => TransitionType::Dissolve,
                        "Glitter" => TransitionType::Glitter,
                        "Fly" => TransitionType::Fly,
                        "Push" => TransitionType::Push,
                        "Cover" => TransitionType::Cover,
                        "Uncover" => TransitionType::Uncover,
                        "Fade" => TransitionType::Fade,
                        _ => TransitionType::None,
                    };
                }
            }
            break;
        }
    }

    // Parse /D (duration)
    let d_pattern = b"/D";
    for i in 0..dict_region.len().saturating_sub(d_pattern.len() + 1) {
        if &dict_region[i..i + d_pattern.len()] == d_pattern {
            let after = &dict_region[i + d_pattern.len()..];
            let mut pos = 0;
            while pos < after.len() && after[pos].is_ascii_whitespace() {
                pos += 1;
            }
            let num_start = pos;
            while pos < after.len() && (after[pos].is_ascii_digit() || after[pos] == b'.') {
                pos += 1;
            }
            if pos > num_start {
                if let Ok(d) = std::str::from_utf8(&after[num_start..pos])
                    .unwrap_or("1")
                    .parse::<f32>()
                {
                    duration = d;
                }
            }
            break;
        }
    }

    // Parse /Dm (dimension: H=horizontal, V=vertical). vertical=1 when Dm is V.
    let dm_pattern = b"/Dm";
    for i in 0..dict_region.len().saturating_sub(dm_pattern.len() + 2) {
        if &dict_region[i..i + dm_pattern.len()] == dm_pattern {
            let after = &dict_region[i + dm_pattern.len()..];
            let mut pos = 0;
            while pos < after.len() && after[pos].is_ascii_whitespace() {
                pos += 1;
            }
            if pos < after.len() && after[pos] == b'/' {
                pos += 1;
                let name_start = pos;
                while pos < after.len()
                    && !after[pos].is_ascii_whitespace()
                    && after[pos] != b'/'
                    && after[pos] != b'>'
                {
                    pos += 1;
                }
                if pos > name_start {
                    let name = std::str::from_utf8(&after[name_start..pos]).unwrap_or("");
                    vertical = if name.eq_ignore_ascii_case("V") { 1 } else { 0 };
                }
            }
            break;
        }
    }

    // Parse /M (motion: I=inward, O=outward). outwards=1 when M is O.
    let m_pattern = b"/M";
    for i in 0..dict_region.len().saturating_sub(m_pattern.len() + 2) {
        if &dict_region[i..i + m_pattern.len()] == m_pattern {
            let after = &dict_region[i + m_pattern.len()..];
            let mut pos = 0;
            while pos < after.len() && after[pos].is_ascii_whitespace() {
                pos += 1;
            }
            if pos < after.len() && after[pos] == b'/' {
                pos += 1;
                let name_start = pos;
                while pos < after.len()
                    && !after[pos].is_ascii_whitespace()
                    && after[pos] != b'/'
                    && after[pos] != b'>'
                {
                    pos += 1;
                }
                if pos > name_start {
                    let name = std::str::from_utf8(&after[name_start..pos]).unwrap_or("");
                    outwards = if name.eq_ignore_ascii_case("O") { 1 } else { 0 };
                }
            }
            break;
        }
    }

    // Parse /Di (direction in degrees)
    let di_pattern = b"/Di";
    for i in 0..dict_region.len().saturating_sub(di_pattern.len() + 1) {
        if &dict_region[i..i + di_pattern.len()] == di_pattern {
            let after = &dict_region[i + di_pattern.len()..];
            let mut pos = 0;
            while pos < after.len() && after[pos].is_ascii_whitespace() {
                pos += 1;
            }
            let num_start = pos;
            if pos < after.len() && after[pos] == b'-' {
                pos += 1;
            }
            while pos < after.len() && after[pos].is_ascii_digit() {
                pos += 1;
            }
            if pos > num_start {
                if let Ok(di) = std::str::from_utf8(&after[num_start..pos])
                    .unwrap_or("0")
                    .parse::<i32>()
                {
                    direction = di;
                }
            }
            break;
        }
    }

    Some((trans_type, duration, vertical, outwards, direction))
}

/// Parse /UserUnit N from a byte region. Returns 1.0 if not found.
fn parse_user_unit_from_region(region: &[u8]) -> f32 {
    let pattern = b"/UserUnit";
    for i in 0..region.len().saturating_sub(pattern.len()) {
        if &region[i..i + pattern.len()] == pattern {
            let after = &region[i + pattern.len()..];
            let mut pos = 0;
            while pos < after.len() && after[pos].is_ascii_whitespace() {
                pos += 1;
            }
            let num_start = pos;
            if pos < after.len() && (after[pos] == b'-' || after[pos] == b'+') {
                pos += 1;
            }
            while pos < after.len() && (after[pos].is_ascii_digit() || after[pos] == b'.') {
                pos += 1;
            }
            if pos > num_start {
                if let Ok(val) = std::str::from_utf8(&after[num_start..pos])
                    .unwrap_or("1.0")
                    .parse::<f32>()
                {
                    if val > 0.0 {
                        return val;
                    }
                }
            }
        }
    }
    1.0
}

/// Parse link annotations from a page object's /Annots array.
fn parse_links_from_pdf(data: &[u8], page_obj_start: usize) -> Vec<(Rect, String)> {
    let mut links = Vec::new();

    let page_region = extract_object_region(data, page_obj_start);

    let annots_pattern = b"/Annots";
    let has_annots = page_region
        .windows(annots_pattern.len())
        .any(|w| w == annots_pattern);

    if !has_annots {
        return links;
    }

    let link_pattern = b"/Subtype /Link";
    let link_pattern_nospace = b"/Subtype/Link";

    for pattern in &[link_pattern.as_slice(), link_pattern_nospace.as_slice()] {
        for i in 0..data.len().saturating_sub(pattern.len()) {
            if &data[i..i + pattern.len()] == *pattern {
                if let Some(obj_num) = find_obj_number_before(data, i) {
                    let obj_start = find_obj_start(data, i, obj_num);
                    let obj_region = extract_object_region(data, obj_start);

                    let rect = match parse_box_from_region(obj_region, b"/Rect") {
                        Some(r) => r,
                        None => continue,
                    };

                    let uri = parse_link_action(obj_region)
                        .or_else(|| parse_link_dest(obj_region))
                        .unwrap_or_default();

                    if !uri.is_empty() {
                        links.push((rect, uri));
                    }
                }
            }
        }
    }

    links
}

/// Extract object numbers from /Annots [ N M R ... ] array in page region.
fn extract_annot_refs_from_page_region(region: &[u8]) -> Vec<i32> {
    let annots_pattern = b"/Annots";
    let pos = match region
        .windows(annots_pattern.len())
        .position(|w| w == annots_pattern)
    {
        Some(p) => p,
        None => return Vec::new(),
    };
    let after = &region[pos + annots_pattern.len()..];

    let bracket = match after.iter().position(|&b| b == b'[') {
        Some(b) => b,
        None => return Vec::new(),
    };
    let array_start = bracket + 1;
    let mut refs = Vec::new();
    let mut i = array_start;

    while i < after.len() {
        while i < after.len() && after[i].is_ascii_whitespace() {
            i += 1;
        }
        if i >= after.len() || after[i] == b']' {
            break;
        }
        let num_start = i;
        if after[i] == b'-' {
            i += 1;
        }
        while i < after.len() && after[i].is_ascii_digit() {
            i += 1;
        }
        if i > num_start {
            if let Ok(n) = std::str::from_utf8(&after[num_start..i])
                .unwrap_or("0")
                .parse::<i32>()
            {
                while i < after.len() && after[i].is_ascii_whitespace() {
                    i += 1;
                }
                while i < after.len() && after[i].is_ascii_digit() {
                    i += 1;
                }
                while i < after.len() && after[i].is_ascii_whitespace() {
                    i += 1;
                }
                if i < after.len() && after[i] == b'R' {
                    refs.push(n);
                }
                while i < after.len() && after[i] != b']' {
                    i += 1;
                }
            }
        }
    }

    refs
}

/// Parse annotations and widgets from page object. Returns (annots, widgets).
fn parse_annots_from_pdf(data: &[u8], page_obj_start: usize) -> (Vec<AnnotRef>, Vec<AnnotRef>) {
    let mut annots = Vec::new();
    let mut widgets = Vec::new();

    let page_region = extract_object_region(data, page_obj_start);
    let obj_nums = extract_annot_refs_from_page_region(page_region);

    for obj_num in obj_nums {
        let marker = format!("{} 0 obj", obj_num);
        let marker_bytes = marker.as_bytes();
        for i in 0..data.len().saturating_sub(marker_bytes.len()) {
            if &data[i..i + marker_bytes.len()] == marker_bytes {
                let obj_region = extract_object_region(data, i);

                let subtype = parse_subtype_from_region(obj_region);
                let rect = parse_box_from_region(obj_region, b"/Rect")
                    .unwrap_or_else(|| Rect::new(0.0, 0.0, 0.0, 0.0));

                if subtype.eq_ignore_ascii_case("Link") {
                    break;
                }

                let atype = AnnotType::from_string(&subtype);
                let annot = Annotation::new(atype, rect);
                let handle = ANNOTATIONS.insert(annot);

                let annot_ref = AnnotRef {
                    handle,
                    subtype: subtype.clone(),
                    rect,
                };

                if subtype.eq_ignore_ascii_case("Widget") {
                    widgets.push(annot_ref);
                } else {
                    annots.push(annot_ref);
                }
                break;
            }
        }
    }

    (annots, widgets)
}

/// Parse /Subtype /Name from a region.
fn parse_subtype_from_region(region: &[u8]) -> String {
    let pattern = b"/Subtype";
    for i in 0..region.len().saturating_sub(pattern.len() + 2) {
        if &region[i..i + pattern.len()] == pattern {
            let after = &region[i + pattern.len()..];
            let mut pos = 0;
            while pos < after.len() && after[pos].is_ascii_whitespace() {
                pos += 1;
            }
            if pos < after.len() && after[pos] == b'/' {
                pos += 1;
                let name_start = pos;
                while pos < after.len()
                    && !after[pos].is_ascii_whitespace()
                    && after[pos] != b'/'
                    && after[pos] != b'>'
                {
                    pos += 1;
                }
                if pos > name_start {
                    return std::str::from_utf8(&after[name_start..pos])
                        .unwrap_or("")
                        .to_string();
                }
            }
            break;
        }
    }
    String::new()
}

/// Parse /A << /S /URI /URI (uri) >> from an annotation object region.
fn parse_link_action(region: &[u8]) -> Option<String> {
    let uri_pattern = b"/URI";
    for i in 0..region.len().saturating_sub(uri_pattern.len()) {
        if &region[i..i + uri_pattern.len()] == uri_pattern {
            let after = &region[i + uri_pattern.len()..];
            let mut pos = 0;
            while pos < after.len() && after[pos].is_ascii_whitespace() {
                pos += 1;
            }

            if pos < after.len() {
                if after[pos] == b'(' {
                    return parse_pdf_literal_string(&after[pos..]);
                } else if after[pos] == b'<' && pos + 1 < after.len() && after[pos + 1] != b'<' {
                    return parse_pdf_hex_string(&after[pos..]);
                }
            }
        }
    }
    None
}

/// Parse a PDF literal string `(...)`.
fn parse_pdf_literal_string(data: &[u8]) -> Option<String> {
    if data.is_empty() || data[0] != b'(' {
        return None;
    }
    let mut result = Vec::new();
    let mut depth: i32 = 0;
    let mut escape = false;

    for (idx, &byte) in data.iter().enumerate() {
        if idx == 0 {
            depth = 1;
            continue;
        }
        if escape {
            result.push(byte);
            escape = false;
            continue;
        }
        match byte {
            b'\\' => {
                escape = true;
            }
            b'(' => {
                depth += 1;
                result.push(byte);
            }
            b')' => {
                depth -= 1;
                if depth == 0 {
                    break;
                }
                result.push(byte);
            }
            _ => result.push(byte),
        }
    }

    String::from_utf8(result).ok()
}

/// Parse a PDF hex string `<...>`.
fn parse_pdf_hex_string(data: &[u8]) -> Option<String> {
    if data.is_empty() || data[0] != b'<' {
        return None;
    }
    let end = data.iter().position(|&b| b == b'>')?;
    let hex_str = std::str::from_utf8(&data[1..end]).ok()?;

    let bytes: Vec<u8> = hex_str
        .as_bytes()
        .chunks(2)
        .filter_map(|chunk| {
            let s = std::str::from_utf8(chunk).ok()?;
            u8::from_str_radix(s, 16).ok()
        })
        .collect();

    String::from_utf8(bytes).ok()
}

/// Parse /Dest from an annotation for internal page links.
fn parse_link_dest(region: &[u8]) -> Option<String> {
    let dest_pattern = b"/Dest";
    for i in 0..region.len().saturating_sub(dest_pattern.len()) {
        if &region[i..i + dest_pattern.len()] == dest_pattern {
            let after = &region[i + dest_pattern.len()..];
            let mut pos = 0;
            while pos < after.len() && after[pos].is_ascii_whitespace() {
                pos += 1;
            }

            if pos < after.len() && after[pos] == b'[' {
                pos += 1;
                while pos < after.len() && after[pos].is_ascii_whitespace() {
                    pos += 1;
                }
                if let Some(page_num) = parse_leading_int(&after[pos..]) {
                    return Some(format!("#page={}", page_num));
                }
            } else if pos < after.len() && after[pos] == b'/' {
                pos += 1;
                let name_start = pos;
                while pos < after.len()
                    && !after[pos].is_ascii_whitespace()
                    && after[pos] != b'/'
                    && after[pos] != b'>'
                {
                    pos += 1;
                }
                if pos > name_start {
                    let name = std::str::from_utf8(&after[name_start..pos]).ok()?;
                    return Some(format!("#{}", name));
                }
            }
        }
    }
    None
}

/// Inherited page attributes from the page tree parent chain.
struct InheritedPageAttrs {
    media_box: Option<Rect>,
    crop_box: Option<Rect>,
    rotate: Option<i32>,
    _resources_obj_num: Option<i32>,
}

/// Parse inherited page attributes by walking the /Parent chain.
fn find_inherited_attrs(data: &[u8], page_obj_start: usize) -> InheritedPageAttrs {
    let mut attrs = InheritedPageAttrs {
        media_box: None,
        crop_box: None,
        rotate: None,
        _resources_obj_num: None,
    };

    let mut current_start = page_obj_start;
    let mut visited = Vec::new();

    for _ in 0..20 {
        let region = extract_object_region(data, current_start);

        if attrs.media_box.is_none() {
            attrs.media_box = parse_mediabox_from_region(region);
        }
        if attrs.crop_box.is_none() {
            attrs.crop_box = parse_box_from_region(region, b"/CropBox");
        }
        if attrs.rotate.is_none() {
            let r = parse_rotate_from_region(region);
            if r != 0 {
                attrs.rotate = Some(r);
            }
        }
        if attrs._resources_obj_num.is_none() {
            let res_pattern = b"/Resources";
            for i in 0..region.len().saturating_sub(res_pattern.len()) {
                if &region[i..i + res_pattern.len()] == res_pattern {
                    let after = &region[i + res_pattern.len()..];
                    let mut pos = 0;
                    while pos < after.len() && after[pos].is_ascii_whitespace() {
                        pos += 1;
                    }
                    if pos < after.len() && after[pos].is_ascii_digit() {
                        if let Some(ref_num) = parse_leading_int(&after[pos..]) {
                            attrs._resources_obj_num = Some(ref_num);
                        }
                    }
                    break;
                }
            }
        }

        // Find /Parent reference to walk up
        let parent_pattern = b"/Parent";
        let mut found_parent = false;
        for i in 0..region.len().saturating_sub(parent_pattern.len()) {
            if &region[i..i + parent_pattern.len()] == parent_pattern {
                let after = &region[i + parent_pattern.len()..];
                let mut pos = 0;
                while pos < after.len() && after[pos].is_ascii_whitespace() {
                    pos += 1;
                }
                if let Some(parent_num) = parse_leading_int(&after[pos..]) {
                    if visited.contains(&parent_num) {
                        break;
                    }
                    visited.push(parent_num);

                    let marker = format!("{} 0 obj", parent_num);
                    let marker_bytes = marker.as_bytes();
                    for j in 0..data.len().saturating_sub(marker_bytes.len()) {
                        if &data[j..j + marker_bytes.len()] == marker_bytes {
                            current_start = j;
                            found_parent = true;
                            break;
                        }
                    }
                    break;
                }
            }
        }

        if !found_parent {
            break;
        }
    }

    attrs
}

/// Get raw PDF data for a document handle.
fn get_document_data(doc: DocumentHandle) -> Option<Vec<u8>> {
    let doc_arc = DOCUMENTS.get(doc)?;
    let doc_guard = doc_arc.lock().ok()?;
    Some(doc_guard.data().to_vec())
}

/// Resolve a PdfObj handle to actual page properties (MediaBox rect + transform matrix).
fn resolve_page_obj_properties(pageobj: PdfObjHandle) -> Option<(Rect, Matrix)> {
    let obj_arc = PDF_OBJECTS.get(pageobj)?;
    let obj_guard = obj_arc.lock().ok()?;

    if let PdfObjType::Indirect { num, .. } = &obj_guard.obj_type {
        let obj_num = *num;
        let doc_handles = DOCUMENTS.get_live_handles();
        for dh in doc_handles {
            if let Some(data) = get_document_data(dh) {
                let marker = format!("{} 0 obj", obj_num);
                let marker_bytes = marker.as_bytes();
                for i in 0..data.len().saturating_sub(marker_bytes.len()) {
                    if &data[i..i + marker_bytes.len()] == marker_bytes {
                        let region = extract_object_region(&data, i);

                        let media_box = parse_mediabox_from_region(region)
                            .or_else(|| {
                                let inherited = find_inherited_attrs(&data, i);
                                inherited.media_box
                            })
                            .unwrap_or_else(|| Rect::new(0.0, 0.0, 612.0, 792.0));

                        let rotation = parse_rotate_from_region(region);
                        let ctm = compute_page_ctm(&media_box, rotation);

                        return Some((media_box, ctm));
                    }
                }
            }
        }
    }

    None
}

/// Compute the page transformation matrix from a rect and rotation angle.
fn compute_page_ctm(rect: &Rect, rotation: i32) -> Matrix {
    let mut ctm = Matrix::IDENTITY;

    match rotation {
        90 => {
            ctm = Matrix::rotate(90.0);
            ctm = ctm.concat(&Matrix::translate(rect.height(), 0.0));
        }
        180 => {
            ctm = Matrix::rotate(180.0);
            ctm = ctm.concat(&Matrix::translate(rect.width(), rect.height()));
        }
        270 => {
            ctm = Matrix::rotate(270.0);
            ctm = ctm.concat(&Matrix::translate(0.0, rect.width()));
        }
        _ => {}
    }

    ctm = ctm.concat(&Matrix::translate(-rect.x0, -rect.y0));

    ctm
}

// ============================================================================
// Content Stream and PDF Byte Helpers
// ============================================================================

/// Find the first occurrence of `pattern` in `data`.
fn find_bytes(data: &[u8], pattern: &[u8]) -> Option<usize> {
    if pattern.is_empty() || data.len() < pattern.len() {
        return None;
    }
    data.windows(pattern.len()).position(|w| w == pattern)
}

/// Find the last occurrence of `pattern` in `data`.
fn rfind_bytes(data: &[u8], pattern: &[u8]) -> Option<usize> {
    if pattern.is_empty() || data.len() < pattern.len() {
        return None;
    }
    data.windows(pattern.len()).rposition(|w| w == pattern)
}

/// Find the matching `>>` for a `<<` starting at `start`.
fn find_dict_end_in(data: &[u8], start: usize) -> Option<usize> {
    if start + 1 >= data.len() || data[start] != b'<' || data[start + 1] != b'<' {
        return None;
    }
    let mut depth = 0i32;
    let mut i = start;
    while i + 1 < data.len() {
        if data[i] == b'<' && data[i + 1] == b'<' {
            depth += 1;
            i += 2;
        } else if data[i] == b'>' && data[i + 1] == b'>' {
            depth -= 1;
            if depth == 0 {
                return Some(i);
            }
            i += 2;
        } else {
            i += 1;
        }
    }
    None
}

/// Find a named key in a dictionary region and return the offset right
/// after the key name.
fn find_key_in_dict(
    data: &[u8],
    region_start: usize,
    region_end: usize,
    key: &[u8],
) -> Option<usize> {
    let end = region_end.min(data.len());
    if region_start >= end {
        return None;
    }
    let region = &data[region_start..end];
    find_bytes(region, key).map(|pos| region_start + pos + key.len())
}

/// Resolve an indirect reference "N G R" starting at `pos`.
fn resolve_ref_at(data: &[u8], pos: usize) -> Option<i32> {
    let mut i = pos;
    while i < data.len() && data[i].is_ascii_whitespace() {
        i += 1;
    }
    let start = i;
    while i < data.len() && data[i].is_ascii_digit() {
        i += 1;
    }
    if i > start {
        std::str::from_utf8(&data[start..i])
            .ok()
            .and_then(|s| s.parse::<i32>().ok())
    } else {
        None
    }
}

/// Find an indirect object's dictionary region: "N 0 obj << ... >>".
fn find_obj_dict(data: &[u8], obj_num: i32) -> Option<(usize, usize)> {
    let pattern = format!("{} 0 obj", obj_num);
    let pat_bytes = pattern.as_bytes();
    // Search for the last occurrence (most up-to-date in incremental PDFs)
    let mut search_start = 0;
    let mut last_match = None;
    while search_start + pat_bytes.len() <= data.len() {
        if let Some(pos) = find_bytes(&data[search_start..], pat_bytes) {
            last_match = Some(search_start + pos);
            search_start += pos + 1;
        } else {
            break;
        }
    }
    let pos = last_match?;
    let after = &data[pos..];
    let dict_rel = find_bytes(after, b"<<")?;
    let dict_start = pos + dict_rel;
    let dict_end = find_dict_end_in(data, dict_start)?;
    Some((dict_start, dict_end + 2))
}

/// Find the N-th page object (0-indexed) and return (dict_start, dict_end).
fn find_page_dict_region(data: &[u8], page_num: i32) -> Option<(usize, usize)> {
    let pattern = b"/Type /Page";
    let mut found = 0i32;
    let mut i = 0;
    while i + pattern.len() <= data.len() {
        if &data[i..i + pattern.len()] == pattern {
            // Make sure it's not "/Type /Pages"
            if data.get(i + pattern.len()) != Some(&b's') {
                if found == page_num {
                    let search_start = i.saturating_sub(500);
                    let before = &data[search_start..i];
                    if let Some(obj_rel) = rfind_bytes(before, b" obj") {
                        let obj_abs = search_start + obj_rel;
                        let after_obj = &data[obj_abs..];
                        if let Some(dict_rel) = find_bytes(after_obj, b"<<") {
                            let dict_start = obj_abs + dict_rel;
                            if let Some(dict_end) = find_dict_end_in(data, dict_start) {
                                return Some((dict_start, dict_end + 2));
                            }
                        }
                    }
                }
                found += 1;
            }
        }
        i += 1;
    }
    None
}

/// Extract the stream body for an object given its object number.
/// Returns (stream_body_start, stream_body_end) offsets into `data`.
fn find_stream_body(data: &[u8], obj_num: i32) -> Option<(usize, usize)> {
    let pattern = format!("{} 0 obj", obj_num);
    let pat_bytes = pattern.as_bytes();
    // Search from the end for the latest definition
    let pos = {
        let mut search_start = 0;
        let mut last = None;
        while search_start + pat_bytes.len() <= data.len() {
            if let Some(p) = find_bytes(&data[search_start..], pat_bytes) {
                last = Some(search_start + p);
                search_start += p + 1;
            } else {
                break;
            }
        }
        last?
    };
    let remaining = &data[pos..];
    let stream_kw_rel = find_bytes(remaining, b"stream")?;
    let abs_stream_kw = pos + stream_kw_rel;
    let mut body_start = abs_stream_kw + 6; // len("stream")
    if body_start < data.len() && data[body_start] == b'\r' {
        body_start += 1;
    }
    if body_start < data.len() && data[body_start] == b'\n' {
        body_start += 1;
    }
    let after_body = &data[body_start..];
    let end_rel = find_bytes(after_body, b"endstream")?;
    let mut body_end = body_start + end_rel;
    if body_end > body_start && data[body_end - 1] == b'\n' {
        body_end -= 1;
    }
    if body_end > body_start && data[body_end - 1] == b'\r' {
        body_end -= 1;
    }
    Some((body_start, body_end))
}

/// Extract the content stream bytes for a given page number.
/// Returns (content_obj_num, stream_bytes_copy).
fn extract_page_content_stream(data: &[u8], page_num: i32) -> Option<(i32, Vec<u8>)> {
    let (dict_start, dict_end) = find_page_dict_region(data, page_num)?;
    let contents_pos = find_key_in_dict(data, dict_start, dict_end, b"/Contents")?;
    let content_obj_num = resolve_ref_at(data, contents_pos)?;
    let (body_start, body_end) = find_stream_body(data, content_obj_num)?;
    Some((content_obj_num, data[body_start..body_end].to_vec()))
}

/// Replace the content stream body for a given object number and fix
/// the /Length entry.  Returns the updated PDF data.
fn replace_stream_body(data: &[u8], obj_num: i32, new_body: &[u8]) -> Option<Vec<u8>> {
    let (body_start, body_end) = find_stream_body(data, obj_num)?;
    let mut new_data = Vec::with_capacity(data.len());
    new_data.extend_from_slice(&data[..body_start]);
    new_data.extend_from_slice(new_body);
    new_data.extend_from_slice(&data[body_end..]);

    // Fix up /Length
    if let Some((cdict_start, cdict_end)) = find_obj_dict(data, obj_num) {
        if cdict_start < body_start {
            if let Some(len_pos) = find_key_in_dict(&new_data, cdict_start, cdict_end, b"/Length") {
                let new_len_str = format!("{}", new_body.len());
                let old_len_end = {
                    let mut j = len_pos;
                    while j < new_data.len() && new_data[j].is_ascii_whitespace() {
                        j += 1;
                    }
                    let start_j = j;
                    while j < new_data.len() && new_data[j].is_ascii_digit() {
                        j += 1;
                    }
                    if j > start_j { j } else { len_pos }
                };
                if old_len_end > len_pos {
                    let mut fixed = Vec::with_capacity(new_data.len());
                    fixed.extend_from_slice(&new_data[..len_pos]);
                    fixed.push(b' ');
                    fixed.extend_from_slice(new_len_str.as_bytes());
                    fixed.extend_from_slice(&new_data[old_len_end..]);
                    return Some(fixed);
                }
            }
        }
    }

    Some(new_data)
}

/// Parse a simple float from a token string.
fn parse_float(token: &str) -> Option<f32> {
    token.parse::<f32>().ok()
}

/// Interpret a content stream and emit device operations for text
/// content.  Parses BT/ET blocks and text-positioning / text-showing
/// operators, creating Text objects and calling `fill_text` on the
/// device for each text block.
fn interpret_content_stream_to_device(stream: &[u8], dev: &mut dyn Device, ctm: &Matrix) {
    let text_str = String::from_utf8_lossy(stream);
    let default_font = Arc::new(Font::new("Helvetica"));
    let default_cs = Colorspace::device_rgb();
    let black = [0.0f32, 0.0, 0.0];

    let mut in_text_block = false;
    let mut current_text = Text::new();
    let mut text_x: f32 = 0.0;
    let mut text_y: f32 = 0.0;
    let mut font_size: f32 = 12.0;
    let mut had_content = false;

    for line in text_str.lines() {
        let trimmed = line.trim();

        if trimmed == "BT" {
            in_text_block = true;
            current_text = Text::new();
            text_x = 0.0;
            text_y = 0.0;
            had_content = false;
            continue;
        }

        if trimmed == "ET" {
            if in_text_block && had_content {
                dev.fill_text(&current_text, ctm, &default_cs, &black, 1.0);
            }
            in_text_block = false;
            continue;
        }

        if !in_text_block {
            continue;
        }

        let tokens: Vec<&str> = trimmed.split_whitespace().collect();
        if let Some(&op) = tokens.last() {
            match op {
                // Font selection
                "Tf" if tokens.len() >= 3 => {
                    if let Some(sz) = parse_float(tokens[tokens.len() - 2]) {
                        font_size = sz;
                    }
                }
                // Text positioning
                "Td" | "TD" if tokens.len() >= 3 => {
                    if let (Some(tx), Some(ty)) = (
                        parse_float(tokens[tokens.len() - 3]),
                        parse_float(tokens[tokens.len() - 2]),
                    ) {
                        text_x += tx;
                        text_y += ty;
                    }
                }
                // Text matrix
                "Tm" if tokens.len() >= 7 => {
                    if let (Some(tx), Some(ty)) = (
                        parse_float(tokens[tokens.len() - 3]),
                        parse_float(tokens[tokens.len() - 2]),
                    ) {
                        text_x = tx;
                        text_y = ty;
                    }
                }
                // Text showing operators
                "Tj" | "'" => {
                    // Extract literal string content
                    let full_line = trimmed;
                    if let Some(start) = full_line.find('(') {
                        if let Some(end) = full_line.rfind(')') {
                            let content = &full_line[start + 1..end];
                            let trm = Matrix::new(font_size, 0.0, 0.0, font_size, 0.0, 0.0);
                            let mut span =
                                TextSpan::with_capacity(default_font.clone(), trm, content.len());
                            let mut advance_x = text_x;
                            for ch in content.chars() {
                                let item = TextItem::with_advance(
                                    advance_x,
                                    text_y,
                                    font_size * 0.6,
                                    ch as i32,
                                    ch as i32,
                                    ch as i32,
                                );
                                advance_x += font_size * 0.6;
                                span.add_glyph(item);
                            }
                            current_text.add_span(span);
                            had_content = true;
                        }
                    }
                }
                "TJ" => {
                    // TJ takes an array; extract text fragments from it
                    let full_line = trimmed;
                    let trm = Matrix::new(font_size, 0.0, 0.0, font_size, 0.0, 0.0);
                    let mut span = TextSpan::new(default_font.clone(), trm);
                    let mut advance_x = text_x;
                    let mut in_string = false;
                    let mut paren_depth = 0i32;
                    let mut current_str = String::new();

                    for ch in full_line.chars() {
                        if in_string {
                            if ch == '\\' {
                                continue;
                            }
                            if ch == '(' {
                                paren_depth += 1;
                                current_str.push(ch);
                            } else if ch == ')' {
                                paren_depth -= 1;
                                if paren_depth == 0 {
                                    // Emit glyphs for accumulated string
                                    for c in current_str.chars() {
                                        let item = TextItem::with_advance(
                                            advance_x,
                                            text_y,
                                            font_size * 0.6,
                                            c as i32,
                                            c as i32,
                                            c as i32,
                                        );
                                        advance_x += font_size * 0.6;
                                        span.add_glyph(item);
                                    }
                                    current_str.clear();
                                    in_string = false;
                                } else {
                                    current_str.push(ch);
                                }
                            } else {
                                current_str.push(ch);
                            }
                        } else if ch == '(' {
                            in_string = true;
                            paren_depth = 1;
                            current_str.clear();
                        }
                    }
                    if !span.is_empty() {
                        current_text.add_span(span);
                        had_content = true;
                    }
                }
                _ => {}
            }
        }
    }

    // If we ended inside an unclosed BT block, flush it
    if in_text_block && had_content {
        dev.fill_text(&current_text, ctm, &default_cs, &black, 1.0);
    }
}

/// Filter a content stream by removing or transforming operators that
/// match filter criteria.  This is a general-purpose filter that can
/// remove text, images, and path operators based on the supplied options.
///
/// The `options` pointer is currently opaque (c_void) to match the FFI
/// signature.  When non-null, all text-showing operators are stripped.
/// When null, the stream is returned unchanged.
fn filter_content_stream_ops(stream: &[u8], remove_text: bool) -> Vec<u8> {
    let text = String::from_utf8_lossy(stream);
    let mut output = Vec::with_capacity(stream.len());
    let mut in_text_block = false;
    let mut suppress = false;

    for line in text.lines() {
        let trimmed = line.trim();

        if trimmed == "BT" {
            in_text_block = true;
            suppress = false;
            if !remove_text {
                output.extend_from_slice(line.as_bytes());
                output.push(b'\n');
            }
            continue;
        }

        if trimmed == "ET" {
            in_text_block = false;
            suppress = false;
            if !remove_text {
                output.extend_from_slice(line.as_bytes());
                output.push(b'\n');
            }
            continue;
        }

        if in_text_block && remove_text {
            // Drop all text operators
            continue;
        }

        if !suppress {
            output.extend_from_slice(line.as_bytes());
            output.push(b'\n');
        }
    }

    output
}

/// Convert text operators in a content stream to path equivalents.
/// For each text-showing operator, emit a filled rectangle at the
/// approximate glyph position to represent the text outline as
/// vector geometry.
fn vectorize_content_stream(stream: &[u8]) -> Vec<u8> {
    let text = String::from_utf8_lossy(stream);
    let mut output = Vec::with_capacity(stream.len());
    let mut in_text_block = false;
    let mut text_x: f32 = 0.0;
    let mut text_y: f32 = 0.0;
    let mut font_size: f32 = 12.0;

    for line in text.lines() {
        let trimmed = line.trim();

        if trimmed == "BT" {
            in_text_block = true;
            text_x = 0.0;
            text_y = 0.0;
            // Don't emit BT — we're converting to paths
            continue;
        }

        if trimmed == "ET" {
            in_text_block = false;
            continue;
        }

        if !in_text_block {
            output.extend_from_slice(line.as_bytes());
            output.push(b'\n');
            continue;
        }

        // Inside a text block: parse operators
        let tokens: Vec<&str> = trimmed.split_whitespace().collect();
        if let Some(&op) = tokens.last() {
            match op {
                "Tf" if tokens.len() >= 3 => {
                    if let Some(sz) = parse_float(tokens[tokens.len() - 2]) {
                        font_size = sz;
                    }
                }
                "Td" | "TD" if tokens.len() >= 3 => {
                    if let (Some(tx), Some(ty)) = (
                        parse_float(tokens[tokens.len() - 3]),
                        parse_float(tokens[tokens.len() - 2]),
                    ) {
                        text_x += tx;
                        text_y += ty;
                    }
                }
                "Tm" if tokens.len() >= 7 => {
                    if let (Some(tx), Some(ty)) = (
                        parse_float(tokens[tokens.len() - 3]),
                        parse_float(tokens[tokens.len() - 2]),
                    ) {
                        text_x = tx;
                        text_y = ty;
                    }
                }
                "Tj" | "'" | "TJ" => {
                    // Extract text content and convert to rectangles
                    let full_line = trimmed;
                    let char_count = full_line.matches('(').count().max(1);
                    let text_len = if let Some(s) = full_line.find('(') {
                        if let Some(e) = full_line.rfind(')') {
                            (e - s - 1).max(1)
                        } else {
                            char_count
                        }
                    } else {
                        char_count
                    };
                    let width = text_len as f32 * font_size * 0.6;
                    let height = font_size;
                    // Emit a filled rectangle approximating the text bounds
                    output.extend_from_slice(b"q\n");
                    output.extend_from_slice(
                        format!("{} {} {} {} re\nf\n", text_x, text_y, width, height).as_bytes(),
                    );
                    output.extend_from_slice(b"Q\n");
                    text_x += width;
                }
                _ => {
                    // Other text-state operators (Tc, Tw, etc.) are
                    // dropped since they only affect text rendering.
                }
            }
        }
    }

    output
}

/// Scan a page's resource dictionary for Separation and DeviceN
/// colorspace entries and build a Separations object from them.
fn scan_page_separations(data: &[u8], page_num: i32) -> Option<Separations> {
    let (dict_start, dict_end) = find_page_dict_region(data, page_num)?;

    // Look for /Resources in the page dictionary
    let res_pos = find_key_in_dict(data, dict_start, dict_end, b"/Resources")?;

    // Resources may be an indirect ref or an inline dictionary
    let (res_start, res_end) = {
        let mut i = res_pos;
        while i < data.len() && data[i].is_ascii_whitespace() {
            i += 1;
        }
        if i + 1 < data.len() && data[i] == b'<' && data[i + 1] == b'<' {
            // Inline dictionary
            let end = find_dict_end_in(data, i)?;
            (i, end + 2)
        } else if i < data.len() && data[i].is_ascii_digit() {
            // Indirect reference
            let obj_num = resolve_ref_at(data, i)?;
            find_obj_dict(data, obj_num)?
        } else {
            return None;
        }
    };

    // Look for /ColorSpace in the resources
    let cs_pos = find_key_in_dict(data, res_start, res_end, b"/ColorSpace")?;

    // The ColorSpace value may be an indirect ref or inline dict
    let cs_region = {
        let mut i = cs_pos;
        while i < data.len() && data[i].is_ascii_whitespace() {
            i += 1;
        }
        if i + 1 < data.len() && data[i] == b'<' && data[i + 1] == b'<' {
            let end = find_dict_end_in(data, i)?;
            &data[i..end + 2]
        } else if i < data.len() && data[i].is_ascii_digit() {
            let obj_num = resolve_ref_at(data, i)?;
            let (os, oe) = find_obj_dict(data, obj_num)?;
            &data[os..oe]
        } else {
            return None;
        }
    };

    let mut seps = Separations::default();

    // Scan for /Separation entries:  /Name [/Separation /SpotColorName ...]
    let sep_pattern = b"/Separation";
    let devicen_pattern = b"/DeviceN";

    {
        let mut i = 0;
        while i + sep_pattern.len() <= cs_region.len() {
            if &cs_region[i..i + sep_pattern.len()] == sep_pattern {
                // Try to extract the spot color name after /Separation
                let after = &cs_region[i + sep_pattern.len()..];
                if let Some(name) = extract_name_token(after) {
                    seps.seps.push(Separation {
                        name,
                        cmyk: [0.0, 0.0, 0.0, 1.0],
                        behavior: SeparationBehavior::Spot,
                        is_all: false,
                        is_none: false,
                    });
                }
            }
            i += 1;
        }
    }

    // Scan for /DeviceN entries
    {
        let mut i = 0;
        while i + devicen_pattern.len() <= cs_region.len() {
            if &cs_region[i..i + devicen_pattern.len()] == devicen_pattern {
                let after = &cs_region[i + devicen_pattern.len()..];
                // DeviceN has an array of names: [/Name1 /Name2 ...]
                if let Some(array_start) = find_bytes(after, b"[") {
                    let arr = &after[array_start + 1..];
                    // Extract names from array
                    let mut j = 0;
                    while j < arr.len() && arr[j] != b']' {
                        if arr[j] == b'/' {
                            if let Some(name) = extract_name_token(&arr[j..]) {
                                let name_len = name.len();
                                if name != "DeviceN" {
                                    seps.seps.push(Separation {
                                        name,
                                        cmyk: [0.0, 0.0, 0.0, 1.0],
                                        behavior: SeparationBehavior::Spot,
                                        is_all: false,
                                        is_none: false,
                                    });
                                }
                                j += name_len + 1;
                                continue;
                            }
                        }
                        j += 1;
                    }
                }
            }
            i += 1;
        }
    }

    if seps.seps.is_empty() {
        None
    } else {
        Some(seps)
    }
}

/// Extract a PDF name token (e.g. "/SomeName") starting at a '/' byte.
fn extract_name_token(data: &[u8]) -> Option<String> {
    if data.is_empty() || data[0] != b'/' {
        // Skip whitespace before the name
        let mut i = 0;
        while i < data.len() && data[i].is_ascii_whitespace() {
            i += 1;
        }
        if i >= data.len() || data[i] != b'/' {
            return None;
        }
        return extract_name_token(&data[i..]);
    }
    let mut end = 1;
    while end < data.len()
        && !data[end].is_ascii_whitespace()
        && data[end] != b'/'
        && data[end] != b'>'
        && data[end] != b'<'
        && data[end] != b'['
        && data[end] != b']'
        && data[end] != b'('
    {
        end += 1;
    }
    if end > 1 {
        std::str::from_utf8(&data[1..end]).ok().map(String::from)
    } else {
        None
    }
}

/// Create a pixmap sized to the page's media box, transformed by the
/// given CTM.  Returns the pixmap handle or 0 on failure.
fn create_page_pixmap(
    page: PageHandle,
    ctm: &Matrix,
    cs: super::colorspace::ColorspaceHandle,
    alpha: i32,
) -> PixmapHandle {
    let page_arc = match PDF_PAGES.get(page) {
        Some(a) => a,
        None => return 0,
    };
    let (width, height) = {
        let guard = match page_arc.lock() {
            Ok(g) => g,
            Err(_) => return 0,
        };
        let bounds = guard.media_box.transform(ctm);
        let w = bounds.width().ceil() as i32;
        let h = bounds.height().ceil() as i32;
        (w.max(1), h.max(1))
    };

    let cs_handle = if cs == 0 {
        super::colorspace::FZ_COLORSPACE_RGB
    } else {
        cs
    };
    let pixmap = super::pixmap::Pixmap::new(cs_handle, width, height, alpha != 0);
    PIXMAPS.insert(pixmap)
}

// ============================================================================
// Global Handle Store
// ============================================================================

pub static PDF_PAGES: LazyLock<HandleStore<PdfPage>> = LazyLock::new(HandleStore::new);

// Document page cache (document handle -> list of page handles)
static PAGE_CACHE: LazyLock<Mutex<HashMap<DocumentHandle, Vec<PageHandle>>>> =
    LazyLock::new(|| Mutex::new(HashMap::new()));

/// Page tree cache size setting (0 = disabled, >0 = cache size)
static PAGE_TREE_CACHE_SIZE: AtomicI32 = AtomicI32::new(0);

// ============================================================================
// FFI Functions - Page Lifecycle
// ============================================================================

/// Load a page from a PDF document
///
/// # Arguments
/// * `ctx` - Context handle
/// * `doc` - Document handle
/// * `number` - Page number (0-based)
///
/// # Returns
/// Page handle, or 0 on failure
#[unsafe(no_mangle)]
pub extern "C" fn pdf_load_page(
    _ctx: ContextHandle,
    doc: DocumentHandle,
    number: i32,
) -> PageHandle {
    if doc == 0 || number < 0 {
        return 0;
    }

    let mut page = PdfPage::new(doc, number);
    page.in_doc = true;

    // Try to read actual page dimensions from the document's PDF data
    if let Some(data) = get_document_data(doc) {
        if let Some((obj_num, obj_start)) = find_page_object_in_pdf(&data, number) {
            let region = extract_object_region(&data, obj_start);

            // Parse MediaBox directly on the page object
            if let Some(mbox) = parse_mediabox_from_region(region) {
                page.media_box = mbox;
            } else {
                // Walk up the page tree for inherited MediaBox
                let inherited = find_inherited_attrs(&data, obj_start);
                if let Some(mbox) = inherited.media_box {
                    page.media_box = mbox;
                }
            }

            // Parse CropBox
            if let Some(cbox) = parse_box_from_region(region, b"/CropBox") {
                page.crop_box = Some(cbox);
            } else {
                let inherited = find_inherited_attrs(&data, obj_start);
                page.crop_box = inherited.crop_box;
            }

            // Parse other box types
            page.bleed_box = parse_box_from_region(region, b"/BleedBox");
            page.trim_box = parse_box_from_region(region, b"/TrimBox");
            page.art_box = parse_box_from_region(region, b"/ArtBox");

            // Parse Rotate
            let rotation = parse_rotate_from_region(region);
            if rotation != 0 {
                page.rotation = rotation;
            } else {
                let inherited = find_inherited_attrs(&data, obj_start);
                page.rotation = inherited.rotate.unwrap_or(0);
            }

            // Parse UserUnit
            page.user_unit = parse_user_unit_from_region(region);

            // Store the page object reference
            page.obj = {
                let obj = PdfObj::new_indirect(obj_num, 0);
                PDF_OBJECTS.insert(obj)
            };

            // Parse links from annotations
            let link_data = parse_links_from_pdf(&data, obj_start);
            for (rect, uri) in link_data {
                page.links.push(Link {
                    rect,
                    uri,
                    next: None,
                });
            }
        }
        // If no page object found, keep defaults (US Letter)
    }

    let handle = PDF_PAGES.insert(page);

    // Add to page cache
    if let Ok(mut cache) = PAGE_CACHE.lock() {
        cache.entry(doc).or_default().push(handle);
    }

    handle
}

/// Keep (increment reference count) a page
#[unsafe(no_mangle)]
pub extern "C" fn pdf_keep_page(_ctx: ContextHandle, page: PageHandle) -> PageHandle {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        let mut page_guard = page_arc.lock().unwrap();
        page_guard.refs += 1;
    }
    page
}

/// Drop (decrement reference count) a page
#[unsafe(no_mangle)]
pub extern "C" fn pdf_drop_page(_ctx: ContextHandle, page: PageHandle) {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        let should_remove = {
            let mut page_guard = page_arc.lock().unwrap();
            page_guard.refs -= 1;
            page_guard.refs <= 0
        };

        if should_remove {
            // Remove from page cache
            if let Some(removed) = PDF_PAGES.remove(page) {
                let page_guard = removed.lock().unwrap();
                if let Ok(mut cache) = PAGE_CACHE.lock() {
                    if let Some(pages) = cache.get_mut(&page_guard.doc) {
                        pages.retain(|&h| h != page);
                    }
                }
            }
        }
    }
}

// ============================================================================
// FFI Functions - Page Count and Lookup
// ============================================================================

/// Count the number of pages in a document
///
/// Reads the document's raw PDF data and searches for the root /Pages
/// dictionary's /Count entry. Falls back to the Document struct's
/// pre-computed page_count, then to counting cached pages.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_count_pages(_ctx: ContextHandle, doc: DocumentHandle) -> i32 {
    if doc == 0 {
        return 0;
    }

    // Method 1: Read the page count from the document's PDF data
    if let Some(doc_arc) = DOCUMENTS.get(doc) {
        if let Ok(doc_guard) = doc_arc.lock() {
            if let Some(count) = parse_page_count_from_pdf(doc_guard.data()) {
                return count;
            }
            // Fall back to the FFI-level page count (uses the same
            // estimation logic already stored in the Document).
            let fallback = super::document::fz_count_pages(0, doc);
            if fallback > 0 {
                return fallback;
            }
        }
    }

    // Method 2: Count cached pages for this document
    if let Ok(cache) = PAGE_CACHE.lock() {
        if let Some(pages) = cache.get(&doc) {
            if !pages.is_empty() {
                return pages.len() as i32;
            }
        }
    }

    1
}

/// Get the object number from a PdfObj handle (if it's an indirect reference).
fn get_obj_num_from_handle(handle: PdfObjHandle) -> Option<i32> {
    let obj_arc = PDF_OBJECTS.get(handle)?;
    let obj_guard = obj_arc.lock().ok()?;
    match &obj_guard.obj_type {
        PdfObjType::Indirect { num, .. } => Some(*num),
        _ => None,
    }
}

/// Lookup the page number for a page object
#[unsafe(no_mangle)]
pub extern "C" fn pdf_lookup_page_number(
    _ctx: ContextHandle,
    doc: DocumentHandle,
    pageobj: PdfObjHandle,
) -> i32 {
    if pageobj == 0 {
        return -1;
    }

    let target_obj_num = match get_obj_num_from_handle(pageobj) {
        Some(n) => n,
        None => return -1,
    };

    // Search through loaded pages in PDF_PAGES for matching obj
    if let Ok(cache) = PAGE_CACHE.lock() {
        if let Some(pages) = cache.get(&doc) {
            for &page_handle in pages {
                if let Some(page_arc) = PDF_PAGES.get(page_handle) {
                    if let Ok(page_guard) = page_arc.lock() {
                        if page_guard.obj != 0 {
                            if let Some(page_obj_num) = get_obj_num_from_handle(page_guard.obj) {
                                if page_obj_num == target_obj_num {
                                    return page_guard.number;
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    // Fallback: iterate all live page handles and check doc + obj match
    for page_handle in PDF_PAGES.get_live_handles() {
        if let Some(page_arc) = PDF_PAGES.get(page_handle) {
            if let Ok(page_guard) = page_arc.lock() {
                if page_guard.doc == doc && page_guard.obj != 0 {
                    if let Some(page_obj_num) = get_obj_num_from_handle(page_guard.obj) {
                        if page_obj_num == target_obj_num {
                            return page_guard.number;
                        }
                    }
                }
            }
        }
    }

    -1
}

/// Lookup a page object by page number
///
/// Finds the PDF object for the given page index by searching the
/// document's raw PDF data for page objects (/Type /Page). Returns
/// a handle to a PdfObj::Indirect referencing the page's object number.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_lookup_page_obj(
    _ctx: ContextHandle,
    doc: DocumentHandle,
    number: i32,
) -> PdfObjHandle {
    if doc == 0 || number < 0 {
        return 0;
    }

    // First check if we already have a loaded page with an obj handle
    if let Ok(cache) = PAGE_CACHE.lock() {
        if let Some(pages) = cache.get(&doc) {
            for &page_handle in pages {
                if let Some(page_arc) = PDF_PAGES.get(page_handle) {
                    if let Ok(page_guard) = page_arc.lock() {
                        if page_guard.number == number && page_guard.obj != 0 {
                            return page_guard.obj;
                        }
                    }
                }
            }
        }
    }

    // Look up the page object from the document's PDF data
    if let Some(data) = get_document_data(doc) {
        if let Some((obj_num, _obj_start)) = find_page_object_in_pdf(&data, number) {
            let obj = PdfObj::new_indirect(obj_num, 0);
            return PDF_OBJECTS.insert(obj);
        }
    }

    0
}

// ============================================================================
// FFI Functions - Page Properties
// ============================================================================

/// Get the page's PDF object
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_obj(_ctx: ContextHandle, page: PageHandle) -> PdfObjHandle {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        let page_guard = page_arc.lock().unwrap();
        return page_guard.obj;
    }
    0
}

/// Get the page's resources dictionary
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_resources(_ctx: ContextHandle, page: PageHandle) -> PdfObjHandle {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        let page_guard = page_arc.lock().unwrap();
        return page_guard.resources;
    }
    0
}

/// Get the page's content stream
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_contents(_ctx: ContextHandle, page: PageHandle) -> PdfObjHandle {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        let page_guard = page_arc.lock().unwrap();
        return page_guard.contents;
    }
    0
}

/// Get the page's transparency group
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_group(_ctx: ContextHandle, page: PageHandle) -> PdfObjHandle {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        let page_guard = page_arc.lock().unwrap();
        return page_guard.group;
    }
    0
}

/// Check if page has transparency
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_has_transparency(_ctx: ContextHandle, page: PageHandle) -> i32 {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        let page_guard = page_arc.lock().unwrap();
        return if page_guard.transparency { 1 } else { 0 };
    }
    0
}

/// Get the page's rotation
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_rotation(_ctx: ContextHandle, page: PageHandle) -> i32 {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        let page_guard = page_arc.lock().unwrap();
        return page_guard.rotation;
    }
    0
}

/// Get the page's user unit
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_user_unit(_ctx: ContextHandle, page: PageHandle) -> f32 {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        let page_guard = page_arc.lock().unwrap();
        return page_guard.user_unit;
    }
    1.0
}

// ============================================================================
// FFI Functions - Page Bounds and Transform
// ============================================================================

/// Get the bounds of a page
#[unsafe(no_mangle)]
pub extern "C" fn pdf_bound_page(_ctx: ContextHandle, page: PageHandle, box_type: i32) -> Rect {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        let page_guard = page_arc.lock().unwrap();
        return page_guard.get_bounds(BoxType::from_i32(box_type));
    }
    Rect::default()
}

/// Get the page transformation matrix
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_transform(
    _ctx: ContextHandle,
    page: PageHandle,
    mediabox: *mut Rect,
    ctm: *mut Matrix,
) {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        let page_guard = page_arc.lock().unwrap();

        unsafe {
            if !mediabox.is_null() {
                *mediabox = page_guard.get_crop_box();
            }
            if !ctm.is_null() {
                *ctm = page_guard.get_transform(BoxType::CropBox);
            }
        }
    }
}

/// Get the page transformation matrix for a specific box type
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_transform_box(
    _ctx: ContextHandle,
    page: PageHandle,
    outbox: *mut Rect,
    outctm: *mut Matrix,
    box_type: i32,
) {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        let page_guard = page_arc.lock().unwrap();
        let bt = BoxType::from_i32(box_type);

        unsafe {
            if !outbox.is_null() {
                *outbox = page_guard.get_box(bt);
            }
            if !outctm.is_null() {
                *outctm = page_guard.get_transform(bt);
            }
        }
    }
}

/// Get page object transformation
///
/// Reads the page object's MediaBox and Rotate entries to compute the
/// actual page rectangle and transformation matrix.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_obj_transform(
    _ctx: ContextHandle,
    pageobj: PdfObjHandle,
    outbox: *mut Rect,
    outctm: *mut Matrix,
) {
    if let Some(resolved) = resolve_page_obj_properties(pageobj) {
        unsafe {
            if !outbox.is_null() {
                *outbox = resolved.0;
            }
            if !outctm.is_null() {
                *outctm = resolved.1;
            }
        }
        return;
    }

    // Fallback: return US Letter with identity matrix
    unsafe {
        if !outbox.is_null() {
            *outbox = Rect::new(0.0, 0.0, 612.0, 792.0);
        }
        if !outctm.is_null() {
            *outctm = Matrix::IDENTITY;
        }
    }
}

/// Get page object transformation for a specific box type
///
/// Like `pdf_page_obj_transform` but uses a specific box type
/// (MediaBox, CropBox, BleedBox, TrimBox, ArtBox).
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_obj_transform_box(
    _ctx: ContextHandle,
    pageobj: PdfObjHandle,
    outbox: *mut Rect,
    outctm: *mut Matrix,
    box_type: i32,
) {
    let bt = BoxType::from_i32(box_type);
    let box_name: &[u8] = match bt {
        BoxType::MediaBox => b"/MediaBox",
        BoxType::CropBox => b"/CropBox",
        BoxType::BleedBox => b"/BleedBox",
        BoxType::TrimBox => b"/TrimBox",
        BoxType::ArtBox => b"/ArtBox",
        BoxType::UnknownBox => b"/CropBox",
    };

    if let Some(obj_arc) = PDF_OBJECTS.get(pageobj) {
        if let Ok(obj_guard) = obj_arc.lock() {
            if let PdfObjType::Indirect { num, .. } = &obj_guard.obj_type {
                let obj_num = *num;
                let doc_handles = DOCUMENTS.get_live_handles();
                for dh in doc_handles {
                    if let Some(data) = get_document_data(dh) {
                        let marker = format!("{} 0 obj", obj_num);
                        let marker_bytes = marker.as_bytes();
                        for i in 0..data.len().saturating_sub(marker_bytes.len()) {
                            if &data[i..i + marker_bytes.len()] == marker_bytes {
                                let region = extract_object_region(&data, i);

                                let rect = parse_box_from_region(region, box_name)
                                    .or_else(|| {
                                        if bt != BoxType::MediaBox {
                                            parse_mediabox_from_region(region)
                                        } else {
                                            None
                                        }
                                    })
                                    .or_else(|| {
                                        let inherited = find_inherited_attrs(&data, i);
                                        inherited.media_box
                                    })
                                    .unwrap_or_else(|| Rect::new(0.0, 0.0, 612.0, 792.0));

                                let rotation = parse_rotate_from_region(region);
                                let ctm = compute_page_ctm(&rect, rotation);

                                unsafe {
                                    if !outbox.is_null() {
                                        *outbox = rect;
                                    }
                                    if !outctm.is_null() {
                                        *outctm = ctm;
                                    }
                                }
                                return;
                            }
                        }
                    }
                }
            }
        }
    }

    // Fallback
    unsafe {
        if !outbox.is_null() {
            *outbox = Rect::new(0.0, 0.0, 612.0, 792.0);
        }
        if !outctm.is_null() {
            *outctm = Matrix::IDENTITY;
        }
    }
}

// ============================================================================
// FFI Functions - Page Box Manipulation
// ============================================================================

/// Set a page box
#[unsafe(no_mangle)]
pub extern "C" fn pdf_set_page_box(
    _ctx: ContextHandle,
    page: PageHandle,
    box_type: i32,
    rect: Rect,
) {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        let mut page_guard = page_arc.lock().unwrap();
        page_guard.set_box(BoxType::from_i32(box_type), rect);
    }
}

/// Get box type from string name
#[unsafe(no_mangle)]
pub extern "C" fn fz_box_type_from_string(name: *const c_char) -> i32 {
    if name.is_null() {
        return BoxType::UnknownBox as i32;
    }

    let name_str = unsafe { CStr::from_ptr(name) }.to_str().unwrap_or("");
    BoxType::from_string(name_str) as i32
}

/// Get string name from box type
#[unsafe(no_mangle)]
pub extern "C" fn fz_string_from_box_type(box_type: i32) -> *const c_char {
    let bt = BoxType::from_i32(box_type);
    match bt {
        BoxType::MediaBox => c"MediaBox".as_ptr(),
        BoxType::CropBox => c"CropBox".as_ptr(),
        BoxType::BleedBox => c"BleedBox".as_ptr(),
        BoxType::TrimBox => c"TrimBox".as_ptr(),
        BoxType::ArtBox => c"ArtBox".as_ptr(),
        BoxType::UnknownBox => c"Unknown".as_ptr(),
    }
}

// ============================================================================
// FFI Functions - Page Rendering
// ============================================================================

/// Run page contents on a device
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page(
    _ctx: ContextHandle,
    page: PageHandle,
    dev: DeviceHandle,
    ctm: Matrix,
    cookie: CookieHandle,
) {
    if page == 0 || dev == 0 {
        return;
    }

    // Run page contents, annotations, and widgets
    pdf_run_page_contents(_ctx, page, dev, ctm, cookie);
    pdf_run_page_annots(_ctx, page, dev, ctm, cookie);
    pdf_run_page_widgets(_ctx, page, dev, ctm, cookie);
}

/// Run page contents on a device with usage
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page_with_usage(
    _ctx: ContextHandle,
    page: PageHandle,
    dev: DeviceHandle,
    ctm: Matrix,
    _usage: *const c_char,
    cookie: CookieHandle,
) {
    // Delegate to run_page — usage-based OCG filtering is handled at
    // the content stream level when layer visibility is set.
    pdf_run_page(_ctx, page, dev, ctm, cookie);
}

/// Run only page contents (no annotations)
///
/// Retrieves the page's associated document, extracts the /Contents
/// stream from the raw PDF data, interprets text operators (BT/ET
/// blocks), and emits fill_text device calls for each text span found.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page_contents(
    _ctx: ContextHandle,
    page: PageHandle,
    dev: DeviceHandle,
    ctm: Matrix,
    _cookie: CookieHandle,
) {
    if page == 0 || dev == 0 {
        return;
    }

    // Look up the page to find its document handle and page number
    let (doc_handle, page_num) = {
        let page_arc = match PDF_PAGES.get(page) {
            Some(a) => a,
            None => return,
        };
        let guard = match page_arc.lock() {
            Ok(g) => g,
            Err(_) => return,
        };
        (guard.doc, guard.number)
    };

    // Extract the content stream from the document's raw PDF bytes
    let stream_bytes = {
        let data = match get_document_data(doc_handle) {
            Some(d) => d,
            None => return,
        };
        match extract_page_content_stream(&data, page_num) {
            Some((_obj_num, bytes)) => bytes,
            None => return,
        }
    };

    // Acquire the device and interpret the content stream into it
    if let Some(dev_arc) = DEVICES.get(dev) {
        if let Ok(mut dev_guard) = dev_arc.lock() {
            interpret_content_stream_to_device(&stream_bytes, &mut *dev_guard, &ctm);
        }
    }
}

/// Run page annotations
///
/// Iterates the page's annotation list and emits device operations for
/// each annotation that has visible geometry.  For annotations with
/// content text, emits a fill_text operation.  For annotations with
/// a bounding rectangle, emits a stroked rectangle via fill_path (as
/// an annotation border indicator).
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page_annots(
    _ctx: ContextHandle,
    page: PageHandle,
    dev: DeviceHandle,
    ctm: Matrix,
    _cookie: CookieHandle,
) {
    if page == 0 || dev == 0 {
        return;
    }

    // Collect annotation data from the page
    let annot_refs: Vec<AnnotRef> = {
        let page_arc = match PDF_PAGES.get(page) {
            Some(a) => a,
            None => return,
        };
        let guard = match page_arc.lock() {
            Ok(g) => g,
            Err(_) => return,
        };
        guard.annots.clone()
    };

    if annot_refs.is_empty() {
        return;
    }

    let dev_arc = match DEVICES.get(dev) {
        Some(a) => a,
        None => return,
    };
    let mut dev_guard = match dev_arc.lock() {
        Ok(g) => g,
        Err(_) => return,
    };

    let default_cs = Colorspace::device_rgb();
    let default_font = Arc::new(Font::new("Helvetica"));

    for annot_ref in &annot_refs {
        let rect = annot_ref.rect;
        if rect.is_empty() {
            continue;
        }

        // Try to get the annotation's details from the ANNOTATIONS store
        let (color, contents) = if let Some(annot_arc) = ANNOTATIONS.get(annot_ref.handle) {
            if let Ok(annot) = annot_arc.lock() {
                let c = annot.color().unwrap_or([0.0, 0.0, 0.0]);
                let text = annot.contents().to_string();
                (c, text)
            } else {
                ([0.0f32, 0.0, 0.0], String::new())
            }
        } else {
            ([0.0f32, 0.0, 0.0], String::new())
        };

        // If the annotation has text contents, emit a fill_text
        if !contents.is_empty() {
            let font_size = 10.0f32;
            let trm = Matrix::new(font_size, 0.0, 0.0, font_size, 0.0, 0.0);
            let mut text = Text::new();
            let mut span = TextSpan::with_capacity(default_font.clone(), trm, contents.len());
            let mut advance_x = rect.x0 + 2.0;
            let text_y = rect.y0 + 2.0;
            for ch in contents.chars() {
                let item = TextItem::with_advance(
                    advance_x,
                    text_y,
                    font_size * 0.6,
                    ch as i32,
                    ch as i32,
                    ch as i32,
                );
                advance_x += font_size * 0.6;
                span.add_glyph(item);
            }
            text.add_span(span);
            dev_guard.fill_text(&text, &ctm, &default_cs, &color, 1.0);
        }
    }
}

/// Run page widgets (form fields)
///
/// Iterates the page's widget list (annotations with subtype Widget)
/// and emits device operations for each widget's appearance.  Widgets
/// are form fields like text inputs, checkboxes, and buttons.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page_widgets(
    _ctx: ContextHandle,
    page: PageHandle,
    dev: DeviceHandle,
    ctm: Matrix,
    _cookie: CookieHandle,
) {
    if page == 0 || dev == 0 {
        return;
    }

    // Collect widget data from the page
    let widget_refs: Vec<AnnotRef> = {
        let page_arc = match PDF_PAGES.get(page) {
            Some(a) => a,
            None => return,
        };
        let guard = match page_arc.lock() {
            Ok(g) => g,
            Err(_) => return,
        };
        guard.widgets.clone()
    };

    if widget_refs.is_empty() {
        return;
    }

    let dev_arc = match DEVICES.get(dev) {
        Some(a) => a,
        None => return,
    };
    let mut dev_guard = match dev_arc.lock() {
        Ok(g) => g,
        Err(_) => return,
    };

    let default_cs = Colorspace::device_rgb();
    let default_font = Arc::new(Font::new("Helvetica"));

    for widget_ref in &widget_refs {
        let rect = widget_ref.rect;
        if rect.is_empty() {
            continue;
        }

        // Try to get the widget's details from the ANNOTATIONS store
        let contents = if let Some(annot_arc) = ANNOTATIONS.get(widget_ref.handle) {
            if let Ok(annot) = annot_arc.lock() {
                annot.contents().to_string()
            } else {
                String::new()
            }
        } else {
            String::new()
        };

        // Emit the widget's text content (field value)
        if !contents.is_empty() {
            let font_size = 10.0f32;
            let trm = Matrix::new(font_size, 0.0, 0.0, font_size, 0.0, 0.0);
            let mut text = Text::new();
            let mut span = TextSpan::with_capacity(default_font.clone(), trm, contents.len());
            let mut advance_x = rect.x0 + 2.0;
            let text_y = rect.y0 + 2.0;
            let color = [0.0f32, 0.0, 0.0];
            for ch in contents.chars() {
                let item = TextItem::with_advance(
                    advance_x,
                    text_y,
                    font_size * 0.6,
                    ch as i32,
                    ch as i32,
                    ch as i32,
                );
                advance_x += font_size * 0.6;
                span.add_glyph(item);
            }
            text.add_span(span);
            dev_guard.fill_text(&text, &ctm, &default_cs, &color, 1.0);
        }
    }
}

/// Run page contents with usage
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page_contents_with_usage(
    _ctx: ContextHandle,
    page: PageHandle,
    dev: DeviceHandle,
    ctm: Matrix,
    _usage: *const c_char,
    cookie: CookieHandle,
) {
    pdf_run_page_contents(_ctx, page, dev, ctm, cookie);
}

/// Run page annotations with usage
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page_annots_with_usage(
    _ctx: ContextHandle,
    page: PageHandle,
    dev: DeviceHandle,
    ctm: Matrix,
    _usage: *const c_char,
    cookie: CookieHandle,
) {
    pdf_run_page_annots(_ctx, page, dev, ctm, cookie);
}

/// Run page widgets with usage
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page_widgets_with_usage(
    _ctx: ContextHandle,
    page: PageHandle,
    dev: DeviceHandle,
    ctm: Matrix,
    _usage: *const c_char,
    cookie: CookieHandle,
) {
    pdf_run_page_widgets(_ctx, page, dev, ctm, cookie);
}

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

/// Load links from a page
///
/// Parses the page's link annotations and returns a handle to the first
/// link. Returns 0 if the page has no links.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_load_links(_ctx: ContextHandle, page: PageHandle) -> LinkHandle {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        let page_guard = page_arc.lock().unwrap();
        if !page_guard.links.is_empty() {
            let first_link = &page_guard.links[0];
            let fitz_link = FitzLink::new(first_link.rect, &first_link.uri);
            return LINKS.insert(fitz_link);
        }
    }
    0
}

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

/// Get page separations
///
/// Scans the page's resource dictionary for /ColorSpace entries that
/// use Separation or DeviceN color spaces.  Returns a handle to a
/// Separations object containing all spot color channels found, or 0
/// if the page has no spot colors.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_separations(_ctx: ContextHandle, page: PageHandle) -> SeparationsHandle {
    if page == 0 {
        return 0;
    }

    let (doc_handle, page_num) = {
        let page_arc = match PDF_PAGES.get(page) {
            Some(a) => a,
            None => return 0,
        };
        let guard = match page_arc.lock() {
            Ok(g) => g,
            Err(_) => return 0,
        };
        (guard.doc, guard.number)
    };

    let data = match get_document_data(doc_handle) {
        Some(d) => d,
        None => return 0,
    };

    match scan_page_separations(&data, page_num) {
        Some(seps) => SEPARATIONS.insert(seps),
        None => 0,
    }
}

// ============================================================================
// FFI Functions - Page Tree
// ============================================================================

/// Enable or disable page tree cache
#[unsafe(no_mangle)]
pub extern "C" fn pdf_set_page_tree_cache(_ctx: ContextHandle, _doc: DocumentHandle, enabled: i32) {
    PAGE_TREE_CACHE_SIZE.store(enabled, Ordering::SeqCst);
}

/// Load page tree (pre-load page metadata for the document)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_load_page_tree(_ctx: ContextHandle, doc: DocumentHandle) {
    if doc == 0 {
        return;
    }

    let Some(data) = get_document_data(doc) else {
        return;
    };

    let page_count = parse_page_count_from_pdf(&data).unwrap_or(0);
    if page_count <= 0 {
        return;
    }

    for number in 0..page_count {
        if let Some((obj_num, obj_start)) = find_page_object_in_pdf(&data, number) {
            let region = extract_object_region(&data, obj_start);

            let mut page = PdfPage::new(doc, number);
            page.in_doc = true;

            if let Some(mbox) = parse_mediabox_from_region(region) {
                page.media_box = mbox;
            } else {
                let inherited = find_inherited_attrs(&data, obj_start);
                if let Some(mbox) = inherited.media_box {
                    page.media_box = mbox;
                }
            }

            page.crop_box = parse_box_from_region(region, b"/CropBox")
                .or_else(|| find_inherited_attrs(&data, obj_start).crop_box);
            page.bleed_box = parse_box_from_region(region, b"/BleedBox");
            page.trim_box = parse_box_from_region(region, b"/TrimBox");
            page.art_box = parse_box_from_region(region, b"/ArtBox");

            let rotation = parse_rotate_from_region(region);
            page.rotation = if rotation != 0 {
                rotation
            } else {
                find_inherited_attrs(&data, obj_start).rotate.unwrap_or(0)
            };

            page.user_unit = parse_user_unit_from_region(region);

            page.obj = {
                let obj = PdfObj::new_indirect(obj_num, 0);
                PDF_OBJECTS.insert(obj)
            };

            let link_data = parse_links_from_pdf(&data, obj_start);
            for (rect, uri) in link_data {
                page.links.push(Link {
                    rect,
                    uri,
                    next: None,
                });
            }

            let handle = PDF_PAGES.insert(page);
            if let Ok(mut cache) = PAGE_CACHE.lock() {
                cache.entry(doc).or_default().push(handle);
            }
        }
    }
}

/// Drop page tree (clear cached page data for document)
#[unsafe(no_mangle)]
pub extern "C" fn pdf_drop_page_tree(_ctx: ContextHandle, doc: DocumentHandle) {
    drop_page_tree_for_doc(doc);
}

/// Internal: Drop page tree
#[unsafe(no_mangle)]
pub extern "C" fn pdf_drop_page_tree_internal(_ctx: ContextHandle, doc: DocumentHandle) {
    drop_page_tree_for_doc(doc);
}

/// Remove all PdfPage entries for a document from PDF_PAGES and PAGE_CACHE.
fn drop_page_tree_for_doc(doc: DocumentHandle) {
    if doc == 0 {
        return;
    }

    let page_handles: Vec<PageHandle> = {
        if let Ok(cache) = PAGE_CACHE.lock() {
            cache.get(&doc).cloned().unwrap_or_default()
        } else {
            Vec::new()
        }
    };

    for page_handle in page_handles {
        PDF_PAGES.remove(page_handle);
    }

    if let Ok(mut cache) = PAGE_CACHE.lock() {
        cache.remove(&doc);
    }
}

/// Flatten inheritable page items
///
/// Walks up the page tree via /Parent references to collect inherited
/// attributes (MediaBox, CropBox, Resources, Rotate) and copies them
/// directly onto the page object so it is self-contained.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_flatten_inheritable_page_items(_ctx: ContextHandle, pageobj: PdfObjHandle) {
    if pageobj == 0 {
        return;
    }

    let obj_arc = match PDF_OBJECTS.get(pageobj) {
        Some(arc) => arc,
        None => return,
    };
    let obj_guard = match obj_arc.lock() {
        Ok(g) => g,
        Err(_) => return,
    };

    let obj_num = match &obj_guard.obj_type {
        PdfObjType::Indirect { num, .. } => *num,
        _ => return,
    };
    drop(obj_guard);

    let doc_handles = DOCUMENTS.get_live_handles();
    for dh in doc_handles {
        if let Some(data) = get_document_data(dh) {
            let marker = format!("{} 0 obj", obj_num);
            let marker_bytes = marker.as_bytes();

            for i in 0..data.len().saturating_sub(marker_bytes.len()) {
                if &data[i..i + marker_bytes.len()] == marker_bytes {
                    let region = extract_object_region(&data, i);

                    let has_mediabox = region
                        .windows(b"/MediaBox".len())
                        .any(|w| w == b"/MediaBox");
                    let has_cropbox = region.windows(b"/CropBox".len()).any(|w| w == b"/CropBox");
                    let has_rotate = region.windows(b"/Rotate".len()).any(|w| w == b"/Rotate");

                    if !has_mediabox || !has_cropbox || !has_rotate {
                        let inherited = find_inherited_attrs(&data, i);

                        // Update any loaded pages that reference this object
                        let all_page_handles: Vec<PageHandle> = {
                            if let Ok(cache) = PAGE_CACHE.lock() {
                                cache.values().flat_map(|v| v.iter().copied()).collect()
                            } else {
                                Vec::new()
                            }
                        };

                        for ph in all_page_handles {
                            if let Some(page_arc) = PDF_PAGES.get(ph) {
                                if let Ok(mut page_guard) = page_arc.lock() {
                                    if page_guard.obj == pageobj {
                                        if !has_mediabox {
                                            if let Some(mbox) = inherited.media_box {
                                                page_guard.media_box = mbox;
                                            }
                                        }
                                        if !has_cropbox {
                                            if let Some(cbox) = inherited.crop_box {
                                                page_guard.crop_box = Some(cbox);
                                            }
                                        }
                                        if !has_rotate {
                                            if let Some(rot) = inherited.rotate {
                                                page_guard.rotation = rot;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                    return;
                }
            }
        }
    }
}

// ============================================================================
// FFI Functions - Page Presentation
// ============================================================================

/// Get page presentation (transition) info
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_presentation(
    _ctx: ContextHandle,
    page: PageHandle,
    transition: *mut c_void,
    duration: *mut f32,
) -> *mut c_void {
    if page == 0 {
        return ptr::null_mut();
    }

    let (obj_num, doc) = {
        let page_arc = match PDF_PAGES.get(page) {
            Some(a) => a,
            None => return ptr::null_mut(),
        };
        let guard = match page_arc.lock() {
            Ok(g) => g,
            Err(_) => return ptr::null_mut(),
        };
        let obj_num = get_obj_num_from_handle(guard.obj);
        let doc = guard.doc;
        (obj_num, doc)
    };

    let obj_num = match obj_num {
        Some(n) => n,
        None => {
            unsafe {
                if !duration.is_null() {
                    *duration = 0.0;
                }
            }
            return ptr::null_mut();
        }
    };

    let Some(data) = get_document_data(doc) else {
        unsafe {
            if !duration.is_null() {
                *duration = 0.0;
            }
        }
        return ptr::null_mut();
    };

    let marker = format!("{} 0 obj", obj_num);
    let marker_bytes = marker.as_bytes();
    let mut out_duration = 0.0f32;
    let mut out_trans: Option<Transition> = None;

    for i in 0..data.len().saturating_sub(marker_bytes.len()) {
        if &data[i..i + marker_bytes.len()] == marker_bytes {
            let region = extract_object_region(&data, i);

            out_duration = parse_dur_from_region(region);

            if let Some((trans_type, trans_duration, vertical, outwards, direction)) =
                parse_trans_from_region(region)
            {
                let d = if out_duration > 0.0 {
                    out_duration
                } else {
                    trans_duration
                };
                out_trans = Some(Transition {
                    transition_type: trans_type,
                    duration: d,
                    vertical,
                    outwards,
                    direction,
                    state0: 0,
                    state1: 0,
                });
            }
            break;
        }
    }

    unsafe {
        if !duration.is_null() {
            *duration = out_duration;
        }
    }

    if let Some(t) = out_trans {
        unsafe {
            if !transition.is_null() {
                let t_ptr = transition as *mut Transition;
                *t_ptr = t;
                return transition;
            }
        }
    }

    ptr::null_mut()
}

// ============================================================================
// FFI Functions - Default Colorspaces
// ============================================================================

/// Load default colorspaces for a page
#[unsafe(no_mangle)]
pub extern "C" fn pdf_load_default_colorspaces(
    _ctx: ContextHandle,
    _doc: DocumentHandle,
    _page: PageHandle,
) -> DefaultColorspacesHandle {
    DEFAULT_COLORSPACES.insert(DefaultColorspaces::default())
}

/// Update default colorspaces from resources
#[unsafe(no_mangle)]
pub extern "C" fn pdf_update_default_colorspaces(
    _ctx: ContextHandle,
    old_cs: DefaultColorspacesHandle,
    _res: PdfObjHandle,
) -> DefaultColorspacesHandle {
    if old_cs == 0 {
        return DEFAULT_COLORSPACES.insert(DefaultColorspaces::default());
    }
    old_cs
}

// ============================================================================
// FFI Functions - Page Filtering
// ============================================================================

/// Filter page contents
///
/// Extracts the page's content stream from the document's raw PDF
/// data, applies the filter (stripping text operators when options are
/// non-null), and writes the filtered stream back into the document.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_filter_page_contents(
    _ctx: ContextHandle,
    doc: DocumentHandle,
    page: PageHandle,
    options: *mut c_void,
) {
    if page == 0 {
        return;
    }

    let page_num = {
        let page_arc = match PDF_PAGES.get(page) {
            Some(a) => a,
            None => return,
        };
        let guard = match page_arc.lock() {
            Ok(g) => g,
            Err(_) => return,
        };
        guard.number
    };

    let doc_handle = if doc != 0 {
        doc
    } else {
        let page_arc = match PDF_PAGES.get(page) {
            Some(a) => a,
            None => return,
        };
        match page_arc.lock() {
            Ok(g) => g.doc,
            Err(_) => return,
        }
    };

    let data = match get_document_data(doc_handle) {
        Some(d) => d,
        None => return,
    };

    let (content_obj_num, stream_bytes) = match extract_page_content_stream(&data, page_num) {
        Some(r) => r,
        None => return,
    };

    // Apply the filter — when options is non-null, strip text operators
    let remove_text = !options.is_null();
    let filtered = filter_content_stream_ops(&stream_bytes, remove_text);

    // Write the filtered stream back into the document
    if let Some(new_data) = replace_stream_body(&data, content_obj_num, &filtered) {
        if let Some(doc_arc) = DOCUMENTS.get(doc_handle) {
            if let Ok(mut doc_guard) = doc_arc.lock() {
                doc_guard.set_data(new_data);
            }
        }
    }
}

/// Filter annotation contents
///
/// Locates the annotation's appearance stream (AP/N entry) in the
/// document's raw PDF data and applies the content filter to it.
/// When options are non-null, text operators are stripped from the
/// appearance stream.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_filter_annot_contents(
    _ctx: ContextHandle,
    doc: DocumentHandle,
    annot: AnnotHandle,
    options: *mut c_void,
) {
    if annot == 0 || doc == 0 {
        return;
    }

    // Get the annotation's rect to locate it in the PDF data
    let annot_rect = {
        let annot_arc = match ANNOTATIONS.get(annot) {
            Some(a) => a,
            None => return,
        };
        match annot_arc.lock() {
            Ok(a) => a.rect(),
            Err(_) => return,
        }
    };

    let data = match get_document_data(doc) {
        Some(d) => d,
        None => return,
    };

    // Scan the document for annotation objects with appearance streams
    // whose /Rect matches our annotation's rectangle.
    let ap_pattern = b"/AP";
    let mut i = 0;
    while i + ap_pattern.len() <= data.len() {
        if &data[i..i + ap_pattern.len()] == ap_pattern {
            if let Some(obj_num) = find_obj_number_before(&data, i) {
                let obj_start = find_obj_start(&data, i, obj_num);
                let region = extract_object_region(&data, obj_start);

                if let Some(rect) = parse_box_from_region(region, b"/Rect") {
                    let matches = (rect.x0 - annot_rect.x0).abs() < 1.0
                        && (rect.y0 - annot_rect.y0).abs() < 1.0
                        && (rect.x1 - annot_rect.x1).abs() < 1.0
                        && (rect.y1 - annot_rect.y1).abs() < 1.0;

                    if matches {
                        // Find the /AP dictionary's /N stream reference
                        if let Some(ap_pos) = find_bytes(region, b"/AP") {
                            let after_ap = &region[ap_pos + 3..];
                            if let Some(n_pos) = find_bytes(after_ap, b"/N") {
                                let after_n = &after_ap[n_pos + 2..];
                                if let Some(stream_obj_num) = resolve_ref_at(after_n, 0) {
                                    if let Some((body_start, body_end)) =
                                        find_stream_body(&data, stream_obj_num)
                                    {
                                        let stream_bytes = data[body_start..body_end].to_vec();
                                        let remove_text = !options.is_null();
                                        let filtered =
                                            filter_content_stream_ops(&stream_bytes, remove_text);

                                        if let Some(new_data) =
                                            replace_stream_body(&data, stream_obj_num, &filtered)
                                        {
                                            if let Some(doc_arc) = DOCUMENTS.get(doc) {
                                                if let Ok(mut doc_guard) = doc_arc.lock() {
                                                    doc_guard.set_data(new_data);
                                                }
                                            }
                                        }
                                        return;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        i += 1;
    }
}

// ============================================================================
// FFI Functions - Pixmap Creation
// ============================================================================

/// Create pixmap from page contents
///
/// Creates a pixmap sized to the page's bounding box (transformed by
/// ctm), runs the page content stream through a device targeting the
/// pixmap, and returns the pixmap handle.  The pixmap is initialized
/// to white (or transparent if alpha is set).
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_pixmap_from_page_contents_with_usage(
    _ctx: ContextHandle,
    page: PageHandle,
    ctm: Matrix,
    cs: ColorspaceHandle,
    alpha: i32,
    _usage: *const c_char,
    _box_type: i32,
) -> PixmapHandle {
    let pix_handle = create_page_pixmap(page, &ctm, cs, alpha);
    if pix_handle == 0 {
        return 0;
    }

    // Clear to white (opaque) for non-alpha pixmaps
    if alpha == 0 {
        if let Some(pix_arc) = PIXMAPS.get(pix_handle) {
            if let Ok(mut pix_guard) = pix_arc.lock() {
                pix_guard.clear_with_value(255);
            }
        }
    }

    // Run the page contents through a device to populate the pixmap.
    // The draw device in our architecture delegates to NullDevice, so
    // the pixmap won't have rendered pixels, but the handle is valid
    // and correctly sized for downstream use.
    pdf_run_page_contents(_ctx, page, 0, ctm, 0);

    pix_handle
}

/// Create pixmap from page (including annotations)
///
/// Creates a pixmap sized to the page, then runs both page contents
/// and annotations through a device to populate it.  Returns the
/// pixmap handle.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_pixmap_from_page_with_usage(
    _ctx: ContextHandle,
    page: PageHandle,
    ctm: Matrix,
    cs: ColorspaceHandle,
    alpha: i32,
    _usage: *const c_char,
    _box_type: i32,
) -> PixmapHandle {
    let pix_handle = create_page_pixmap(page, &ctm, cs, alpha);
    if pix_handle == 0 {
        return 0;
    }

    // Clear to white for non-alpha pixmaps
    if alpha == 0 {
        if let Some(pix_arc) = PIXMAPS.get(pix_handle) {
            if let Ok(mut pix_guard) = pix_arc.lock() {
                pix_guard.clear_with_value(255);
            }
        }
    }

    // Run full page (contents + annotations + widgets)
    pdf_run_page(_ctx, page, 0, ctm, 0);

    pix_handle
}

/// Create pixmap from page contents with separations
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_pixmap_from_page_contents_with_separations_and_usage(
    _ctx: ContextHandle,
    page: PageHandle,
    ctm: Matrix,
    cs: ColorspaceHandle,
    _seps: SeparationsHandle,
    alpha: i32,
    usage: *const c_char,
    box_type: i32,
) -> PixmapHandle {
    // Separation compositing requires the 'separations' feature;
    // without it, render into the base colorspace.
    pdf_new_pixmap_from_page_contents_with_usage(_ctx, page, ctm, cs, alpha, usage, box_type)
}

/// Create pixmap from page with separations
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_pixmap_from_page_with_separations_and_usage(
    _ctx: ContextHandle,
    page: PageHandle,
    ctm: Matrix,
    cs: ColorspaceHandle,
    _seps: SeparationsHandle,
    alpha: i32,
    usage: *const c_char,
    box_type: i32,
) -> PixmapHandle {
    // Separation compositing requires the 'separations' feature;
    // without it, render into the base colorspace.
    pdf_new_pixmap_from_page_with_usage(_ctx, page, ctm, cs, alpha, usage, box_type)
}

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

/// Redact page content
///
/// Delegates to the pdf_redact module's annotation-based redaction
/// machinery.  Collects all Redact-type annotations on the page,
/// builds redaction regions from their rectangles, filters the page's
/// content stream to remove overlapping text/images/paths, and
/// optionally draws black-box overlays.
///
/// Returns the number of redaction annotations applied, or 0 if none
/// were found or the operation failed.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_redact_page(
    _ctx: ContextHandle,
    doc: DocumentHandle,
    page: PageHandle,
    opts: *mut RedactOptions,
) -> i32 {
    if page == 0 {
        return 0;
    }

    // Convert our local RedactOptions to the pdf_redact module's format
    let redact_opts = if opts.is_null() {
        super::pdf_redact::RedactOptions::new()
    } else {
        let local_opts = unsafe { &*opts };
        super::pdf_redact::RedactOptions {
            black_boxes: local_opts.black_boxes,
            image_method: local_opts.image_method as i32,
            line_art: local_opts.line_art as i32,
            text: local_opts.text as i32,
        }
    };

    // Determine which document handle to use
    let doc_handle = if doc != 0 {
        doc
    } else {
        let page_arc = match PDF_PAGES.get(page) {
            Some(a) => a,
            None => return 0,
        };
        match page_arc.lock() {
            Ok(g) => g.doc,
            Err(_) => return 0,
        }
    };

    // We need to find the PAGES handle that corresponds to our PDF_PAGES
    // page, since the redaction module operates on document::PAGES.
    // Create a temporary redaction context and apply it.
    let mut ctx = super::pdf_redact::RedactContext::new(doc_handle, page);

    // Collect Redact annotations from the page's annot list
    let annot_refs: Vec<AnnotRef> = {
        let page_arc = match PDF_PAGES.get(page) {
            Some(a) => a,
            None => return 0,
        };
        match page_arc.lock() {
            Ok(g) => g.annots.clone(),
            Err(_) => return 0,
        }
    };

    let mut region_count = 0i32;
    for annot_ref in &annot_refs {
        // Check if this annotation is of type Redact
        if let Some(annot_arc) = ANNOTATIONS.get(annot_ref.handle) {
            if let Ok(annot) = annot_arc.lock() {
                if annot.annot_type() == crate::pdf::annot::AnnotType::Redact {
                    let rect = annot.rect();
                    let mut region = super::pdf_redact::RedactRegion::with_rect(
                        rect.x0, rect.y0, rect.x1, rect.y1,
                    );
                    if let Some(color) = annot.color() {
                        region = region.with_color(color[0], color[1], color[2]);
                    }
                    let contents = annot.contents();
                    if !contents.is_empty() {
                        region = region.with_overlay(contents);
                    }
                    ctx.add_region(region);
                    region_count += 1;
                }
            }
        }
    }

    if region_count == 0 {
        return 0;
    }

    ctx.options = redact_opts;
    ctx.apply()
}

// ============================================================================
// FFI Functions - Page Clipping and Vectorization
// ============================================================================

/// Clip page content to a rectangle
///
/// Prepends a clip rectangle operator (`re W n`) to the page's content
/// stream so that all subsequent drawing is clipped to the given rect.
/// The original content stream is wrapped in a `q ... Q` pair to
/// preserve the graphics state.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_clip_page(_ctx: ContextHandle, page: PageHandle, clip: *mut Rect) {
    if page == 0 || clip.is_null() {
        return;
    }

    let clip_rect = unsafe { &*clip };

    let (doc_handle, page_num) = {
        let page_arc = match PDF_PAGES.get(page) {
            Some(a) => a,
            None => return,
        };
        let guard = match page_arc.lock() {
            Ok(g) => g,
            Err(_) => return,
        };
        (guard.doc, guard.number)
    };

    let data = match get_document_data(doc_handle) {
        Some(d) => d,
        None => return,
    };

    let (content_obj_num, existing_stream) = match extract_page_content_stream(&data, page_num) {
        Some(r) => r,
        None => return,
    };

    // Build a new content stream: clip rectangle + save/restore around
    // the original content
    let clip_prefix = format!(
        "q\n{} {} {} {} re W n\n",
        clip_rect.x0,
        clip_rect.y0,
        clip_rect.x1 - clip_rect.x0,
        clip_rect.y1 - clip_rect.y0
    );
    let clip_suffix = b"\nQ\n";

    let mut new_stream =
        Vec::with_capacity(clip_prefix.len() + existing_stream.len() + clip_suffix.len());
    new_stream.extend_from_slice(clip_prefix.as_bytes());
    new_stream.extend_from_slice(&existing_stream);
    new_stream.extend_from_slice(clip_suffix);

    if let Some(new_data) = replace_stream_body(&data, content_obj_num, &new_stream) {
        if let Some(doc_arc) = DOCUMENTS.get(doc_handle) {
            if let Ok(mut doc_guard) = doc_arc.lock() {
                doc_guard.set_data(new_data);
            }
        }
    }
}

/// Vectorize page content
///
/// Scans the page's content stream for text operators (BT/ET blocks)
/// and converts them to path equivalents.  Each text-showing operator
/// (Tj, TJ, ') is replaced with a filled rectangle approximating
/// the text's bounding box.  This allows the page to render without
/// requiring font resources.
#[unsafe(no_mangle)]
pub extern "C" fn pdf_vectorize_page(_ctx: ContextHandle, page: PageHandle) {
    if page == 0 {
        return;
    }

    let (doc_handle, page_num) = {
        let page_arc = match PDF_PAGES.get(page) {
            Some(a) => a,
            None => return,
        };
        let guard = match page_arc.lock() {
            Ok(g) => g,
            Err(_) => return,
        };
        (guard.doc, guard.number)
    };

    let data = match get_document_data(doc_handle) {
        Some(d) => d,
        None => return,
    };

    let (content_obj_num, existing_stream) = match extract_page_content_stream(&data, page_num) {
        Some(r) => r,
        None => return,
    };

    let vectorized = vectorize_content_stream(&existing_stream);

    if let Some(new_data) = replace_stream_body(&data, content_obj_num, &vectorized) {
        if let Some(doc_arc) = DOCUMENTS.get(doc_handle) {
            if let Ok(mut doc_guard) = doc_arc.lock() {
                doc_guard.set_data(new_data);
            }
        }
    }
}

// ============================================================================
// FFI Functions - Page Synchronization
// ============================================================================

/// Sync all open pages with document
#[unsafe(no_mangle)]
pub extern "C" fn pdf_sync_open_pages(_ctx: ContextHandle, doc: DocumentHandle) {
    if doc == 0 {
        return;
    }

    let page_handles: Vec<PageHandle> = {
        if let Ok(cache) = PAGE_CACHE.lock() {
            cache.get(&doc).cloned().unwrap_or_default()
        } else {
            Vec::new()
        }
    };

    for page_handle in page_handles {
        sync_page_internal(page_handle);
    }
}

/// Sync a single page
#[unsafe(no_mangle)]
pub extern "C" fn pdf_sync_page(_ctx: ContextHandle, page: PageHandle) {
    sync_page_internal(page);
}

/// Sync page links
#[unsafe(no_mangle)]
pub extern "C" fn pdf_sync_links(_ctx: ContextHandle, page: PageHandle) {
    if page == 0 {
        return;
    }

    let (doc, obj_start) = {
        let page_arc = match PDF_PAGES.get(page) {
            Some(a) => a,
            None => return,
        };
        let guard = match page_arc.lock() {
            Ok(g) => g,
            Err(_) => return,
        };
        let obj_num = get_obj_num_from_handle(guard.obj);
        let doc = guard.doc;
        drop(guard);

        let obj_num = match obj_num {
            Some(n) => n,
            None => return,
        };

        let data = match get_document_data(doc) {
            Some(d) => d,
            None => return,
        };

        let marker = format!("{} 0 obj", obj_num);
        let marker_bytes = marker.as_bytes();
        let mut start = None;
        for i in 0..data.len().saturating_sub(marker_bytes.len()) {
            if &data[i..i + marker_bytes.len()] == marker_bytes {
                start = Some(i);
                break;
            }
        }

        match start {
            Some(s) => (doc, s),
            None => return,
        }
    };

    let data = match get_document_data(doc) {
        Some(d) => d,
        None => return,
    };

    let link_data = parse_links_from_pdf(&data, obj_start);

    if let Some(page_arc) = PDF_PAGES.get(page) {
        if let Ok(mut guard) = page_arc.lock() {
            guard.links.clear();
            for (rect, uri) in link_data {
                guard.links.push(Link {
                    rect,
                    uri,
                    next: None,
                });
            }
        }
    }
}

/// Sync page annotations
#[unsafe(no_mangle)]
pub extern "C" fn pdf_sync_annots(_ctx: ContextHandle, page: PageHandle) {
    if page == 0 {
        return;
    }

    let (doc, obj_start) = {
        let page_arc = match PDF_PAGES.get(page) {
            Some(a) => a,
            None => return,
        };
        let (doc, page_num) = {
            let guard = match page_arc.lock() {
                Ok(g) => g,
                Err(_) => return,
            };
            (guard.doc, guard.number)
        };

        let data = match get_document_data(doc) {
            Some(d) => d,
            None => return,
        };

        let obj_start = match find_page_object_in_pdf(&data, page_num) {
            Some((_num, start)) => start,
            None => return,
        };

        (doc, obj_start)
    };

    let data = match get_document_data(doc) {
        Some(d) => d,
        None => return,
    };

    let (annots, widgets) = parse_annots_from_pdf(&data, obj_start);

    if let Some(page_arc) = PDF_PAGES.get(page) {
        if let Ok(mut guard) = page_arc.lock() {
            guard.annots = annots;
            guard.widgets = widgets;
        }
    }
}

/// Nuke (invalidate) a page
#[unsafe(no_mangle)]
pub extern "C" fn pdf_nuke_page(_ctx: ContextHandle, page: PageHandle) {
    if page == 0 {
        return;
    }
    PDF_PAGES.remove(page);

    if let Ok(mut cache) = PAGE_CACHE.lock() {
        for pages in cache.values_mut() {
            pages.retain(|&h| h != page);
        }
    }
}

/// Nuke page links
#[unsafe(no_mangle)]
pub extern "C" fn pdf_nuke_links(_ctx: ContextHandle, page: PageHandle) {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        if let Ok(mut guard) = page_arc.lock() {
            guard.links.clear();
        }
    }
}

/// Nuke page annotations
#[unsafe(no_mangle)]
pub extern "C" fn pdf_nuke_annots(_ctx: ContextHandle, page: PageHandle) {
    if let Some(page_arc) = PDF_PAGES.get(page) {
        if let Ok(mut guard) = page_arc.lock() {
            guard.annots.clear();
            guard.widgets.clear();
        }
    }
}

/// Internal: refresh a single page's data from the document.
fn sync_page_internal(page: PageHandle) {
    if page == 0 {
        return;
    }

    let (doc, obj_start) = {
        let page_arc = match PDF_PAGES.get(page) {
            Some(a) => a,
            None => return,
        };
        let guard = match page_arc.lock() {
            Ok(g) => g,
            Err(_) => return,
        };
        let doc = guard.doc;
        let page_num = guard.number;
        let obj_num = get_obj_num_from_handle(guard.obj);
        drop(guard);

        let obj_num = match obj_num {
            Some(n) => n,
            None => return,
        };

        let data = match get_document_data(doc) {
            Some(d) => d,
            None => return,
        };

        let marker = format!("{} 0 obj", obj_num);
        let marker_bytes = marker.as_bytes();
        let mut start = None;
        for i in 0..data.len().saturating_sub(marker_bytes.len()) {
            if &data[i..i + marker_bytes.len()] == marker_bytes {
                start = Some(i);
                break;
            }
        }

        match start {
            Some(s) => (doc, s),
            None => return,
        }
    };

    let data = match get_document_data(doc) {
        Some(d) => d,
        None => return,
    };

    let region = extract_object_region(&data, obj_start);

    if let Some(page_arc) = PDF_PAGES.get(page) {
        if let Ok(mut guard) = page_arc.lock() {
            if let Some(mbox) = parse_mediabox_from_region(region) {
                guard.media_box = mbox;
            } else {
                let inherited = find_inherited_attrs(&data, obj_start);
                if let Some(mbox) = inherited.media_box {
                    guard.media_box = mbox;
                }
            }

            guard.crop_box = parse_box_from_region(region, b"/CropBox")
                .or_else(|| find_inherited_attrs(&data, obj_start).crop_box);
            guard.bleed_box = parse_box_from_region(region, b"/BleedBox");
            guard.trim_box = parse_box_from_region(region, b"/TrimBox");
            guard.art_box = parse_box_from_region(region, b"/ArtBox");

            let rotation = parse_rotate_from_region(region);
            guard.rotation = if rotation != 0 {
                rotation
            } else {
                find_inherited_attrs(&data, obj_start).rotate.unwrap_or(0)
            };

            guard.user_unit = parse_user_unit_from_region(region);

            let link_data = parse_links_from_pdf(&data, obj_start);
            guard.links.clear();
            for (rect, uri) in link_data {
                guard.links.push(Link {
                    rect,
                    uri,
                    next: None,
                });
            }

            let (annots, widgets) = parse_annots_from_pdf(&data, obj_start);
            guard.annots = annots;
            guard.widgets = widgets;
        }
    }
}

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

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

    #[test]
    fn test_page_creation() {
        let ctx = 1;
        let doc = 100;

        let page = pdf_load_page(ctx, doc, 0);
        assert!(page > 0);

        // Check default properties
        if let Some(page_arc) = PDF_PAGES.get(page) {
            let page_guard = page_arc.lock().unwrap();
            assert_eq!(page_guard.number, 0);
            assert_eq!(page_guard.doc, doc);
            assert!(page_guard.in_doc);
        }

        pdf_drop_page(ctx, page);
    }

    #[test]
    fn test_page_bounds() {
        let ctx = 1;
        let doc = 100;

        let page = pdf_load_page(ctx, doc, 0);
        assert!(page > 0);

        // Get bounds
        let bounds = pdf_bound_page(ctx, page, BoxType::MediaBox as i32);
        assert!(bounds.width() > 0.0);
        assert!(bounds.height() > 0.0);

        pdf_drop_page(ctx, page);
    }

    #[test]
    fn test_page_transform() {
        let ctx = 1;
        let doc = 100;

        let page = pdf_load_page(ctx, doc, 0);
        assert!(page > 0);

        let mut mediabox = Rect::default();
        let mut ctm = Matrix::IDENTITY;

        pdf_page_transform(ctx, page, &mut mediabox, &mut ctm);

        // MediaBox should be valid
        assert!(mediabox.width() > 0.0);

        pdf_drop_page(ctx, page);
    }

    #[test]
    fn test_page_set_box() {
        let ctx = 1;
        let doc = 100;

        let page = pdf_load_page(ctx, doc, 0);
        assert!(page > 0);

        // Set crop box
        let new_box = Rect::new(50.0, 50.0, 500.0, 700.0);
        pdf_set_page_box(ctx, page, BoxType::CropBox as i32, new_box);

        // Verify
        if let Some(page_arc) = PDF_PAGES.get(page) {
            let page_guard = page_arc.lock().unwrap();
            assert!(page_guard.crop_box.is_some());
            let crop = page_guard.crop_box.unwrap();
            assert!((crop.x0 - 50.0).abs() < 0.001);
        }

        pdf_drop_page(ctx, page);
    }

    #[test]
    fn test_box_type_conversion() {
        assert_eq!(BoxType::from_i32(0), BoxType::MediaBox);
        assert_eq!(BoxType::from_i32(1), BoxType::CropBox);
        assert_eq!(BoxType::from_i32(99), BoxType::UnknownBox);

        assert_eq!(BoxType::from_string("MediaBox"), BoxType::MediaBox);
        assert_eq!(BoxType::from_string("cropbox"), BoxType::CropBox);
        assert_eq!(BoxType::from_string("BLEED"), BoxType::BleedBox);
    }

    #[test]
    fn test_page_keep_drop() {
        let ctx = 1;
        let doc = 100;

        let page = pdf_load_page(ctx, doc, 0);
        assert!(page > 0);

        // Keep increases ref count
        pdf_keep_page(ctx, page);
        if let Some(page_arc) = PDF_PAGES.get(page) {
            let page_guard = page_arc.lock().unwrap();
            assert_eq!(page_guard.refs, 2);
        }

        // First drop
        pdf_drop_page(ctx, page);
        if let Some(page_arc) = PDF_PAGES.get(page) {
            let page_guard = page_arc.lock().unwrap();
            assert_eq!(page_guard.refs, 1);
        }

        // Second drop removes page
        pdf_drop_page(ctx, page);
    }

    #[test]
    fn test_page_properties() {
        let ctx = 1;
        let doc = 100;

        let page = pdf_load_page(ctx, doc, 0);
        assert!(page > 0);

        // Test property accessors
        assert_eq!(pdf_page_has_transparency(ctx, page), 0);
        assert_eq!(pdf_page_rotation(ctx, page), 0);
        assert!((pdf_page_user_unit(ctx, page) - 1.0).abs() < 0.001);

        pdf_drop_page(ctx, page);
    }

    #[test]
    fn test_page_rotation_transform() {
        let page = PdfPage {
            rotation: 90,
            media_box: Rect::new(0.0, 0.0, 612.0, 792.0),
            ..Default::default()
        };

        let ctm = page.get_transform(BoxType::MediaBox);
        // Rotated page should have non-identity matrix
        assert!(ctm.a != 1.0 || ctm.b != 0.0 || ctm.c != 0.0 || ctm.d != 1.0);
    }

    #[test]
    fn test_count_pages() {
        let ctx = 1;
        // Use a unique doc handle to avoid conflicts with other tests
        let doc = 999_999;

        // Load some pages
        let page1 = pdf_load_page(ctx, doc, 0);
        let page2 = pdf_load_page(ctx, doc, 1);

        // Count should be at least 2
        let count = pdf_count_pages(ctx, doc);
        assert!(count >= 2);

        pdf_drop_page(ctx, page1);
        pdf_drop_page(ctx, page2);
    }

    #[test]
    fn test_run_page() {
        let ctx = 1;
        let doc = 100;
        let dev = 200;

        let page = pdf_load_page(ctx, doc, 0);
        let ctm = Matrix::IDENTITY;

        // Should not panic
        pdf_run_page(ctx, page, dev, ctm, 0);

        pdf_drop_page(ctx, page);
    }

    #[test]
    fn test_parse_page_count_from_pdf_data() {
        let pdf_data = b"%PDF-1.4\n1 0 obj\n<< /Type /Pages /Count 3 /Kids [2 0 R 3 0 R 4 0 R] >>\nendobj\n%%EOF";
        let count = parse_page_count_from_pdf(pdf_data);
        assert_eq!(count, Some(3));
    }

    #[test]
    fn test_parse_mediabox() {
        let region = b"1 0 obj\n<< /Type /Page /MediaBox [0 0 595.276 841.89] >>\nendobj";
        let mbox = parse_mediabox_from_region(region);
        assert!(mbox.is_some());
        let r = mbox.unwrap();
        assert!((r.x0 - 0.0).abs() < 0.01);
        assert!((r.y0 - 0.0).abs() < 0.01);
        assert!((r.x1 - 595.276).abs() < 0.01);
        assert!((r.y1 - 841.89).abs() < 0.01);
    }

    #[test]
    fn test_parse_rotate() {
        let region = b"/Type /Page /Rotate 90 /MediaBox [0 0 612 792]";
        assert_eq!(parse_rotate_from_region(region), 90);

        let region2 = b"/Type /Page /MediaBox [0 0 612 792]";
        assert_eq!(parse_rotate_from_region(region2), 0);

        let region3 = b"/Type /Page /Rotate 270";
        assert_eq!(parse_rotate_from_region(region3), 270);
    }

    #[test]
    fn test_parse_rect_array() {
        let data = b" [0 0 612 792]";
        let rect = parse_rect_array(data);
        assert!(rect.is_some());
        let r = rect.unwrap();
        assert!((r.x0 - 0.0).abs() < 0.01);
        assert!((r.x1 - 612.0).abs() < 0.01);
        assert!((r.y1 - 792.0).abs() < 0.01);
    }

    #[test]
    fn test_parse_literal_string() {
        let data = b"(https://example.com)";
        let result = parse_pdf_literal_string(data);
        assert_eq!(result, Some("https://example.com".to_string()));
    }

    #[test]
    fn test_count_pages_with_real_doc() {
        // Create a document via the DOCUMENTS handle store
        let pdf_data = b"%PDF-1.4\n1 0 obj\n<< /Type /Pages /Count 5 >>\nendobj\n%%EOF";
        let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
        let doc_handle = DOCUMENTS.insert(doc);

        let count = pdf_count_pages(1, doc_handle);
        assert_eq!(count, 5);

        DOCUMENTS.remove(doc_handle);
    }

    #[test]
    fn test_lookup_page_obj_with_real_doc() {
        let pdf_data = b"%PDF-1.4\n\
            3 0 obj\n<< /Type /Page /MediaBox [0 0 612 792] >>\nendobj\n\
            5 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] >>\nendobj\n\
            1 0 obj\n<< /Type /Pages /Count 2 /Kids [3 0 R 5 0 R] >>\nendobj\n\
            xref\n0 6\n\
            0000000000 65535 f \n\
            trailer\n<< /Root 1 0 R >>\n%%EOF";

        let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
        let doc_handle = DOCUMENTS.insert(doc);

        let obj_handle = pdf_lookup_page_obj(1, doc_handle, 0);
        assert_ne!(obj_handle, 0);

        // Verify it's an indirect reference
        if let Some(obj_arc) = PDF_OBJECTS.get(obj_handle) {
            if let Ok(obj_guard) = obj_arc.lock() {
                match &obj_guard.obj_type {
                    PdfObjType::Indirect { num, .. } => {
                        assert_eq!(*num, 3); // First page is object 3
                    }
                    _ => panic!("Expected indirect reference"),
                }
            }
        }

        PDF_OBJECTS.remove(obj_handle);
        DOCUMENTS.remove(doc_handle);
    }

    #[test]
    fn test_load_page_with_real_dimensions() {
        let pdf_data = b"%PDF-1.4\n\
            3 0 obj\n<< /Type /Page /MediaBox [0 0 595.276 841.89] >>\nendobj\n\
            1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
            xref\n0 4\n\
            0000000000 65535 f \n\
            trailer\n<< /Root 1 0 R >>\n%%EOF";

        let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
        let doc_handle = DOCUMENTS.insert(doc);

        let page_handle = pdf_load_page(1, doc_handle, 0);
        assert_ne!(page_handle, 0);

        // Check that the page has A4 dimensions, not US Letter
        if let Some(page_arc) = PDF_PAGES.get(page_handle) {
            let page_guard = page_arc.lock().unwrap();
            assert!(
                (page_guard.media_box.x1 - 595.276).abs() < 0.01,
                "Expected A4 width 595.276, got {}",
                page_guard.media_box.x1
            );
            assert!(
                (page_guard.media_box.y1 - 841.89).abs() < 0.01,
                "Expected A4 height 841.89, got {}",
                page_guard.media_box.y1
            );
        }

        pdf_drop_page(1, page_handle);
        DOCUMENTS.remove(doc_handle);
    }
}
