//! C FFI for document - MicroPDF compatible
//! Safe Rust implementation using handle-based resource management

use super::outline::OUTLINES;
use super::{DOCUMENTS, Handle, HandleStore, STREAMS};
use crate::enhanced::object_stream::get_page_count_from_object_streams;
use std::ffi::{c_char, c_float};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::LazyLock;

// ============================================================================
// PDF raw-byte parsing helpers (operate on &[u8] document data)
// ============================================================================

/// Find all occurrences of a byte pattern in data.
fn find_all_patterns(data: &[u8], pattern: &[u8]) -> Vec<usize> {
    let mut positions = Vec::new();
    if pattern.is_empty() || data.len() < pattern.len() {
        return positions;
    }
    for i in 0..=data.len() - pattern.len() {
        if &data[i..i + pattern.len()] == pattern {
            positions.push(i);
        }
    }
    positions
}

/// Find the first occurrence of a byte pattern.
fn find_pattern(data: &[u8], pattern: &[u8]) -> Option<usize> {
    if pattern.is_empty() || data.len() < pattern.len() {
        return None;
    }
    for i in 0..=data.len() - pattern.len() {
        if &data[i..i + pattern.len()] == pattern {
            return Some(i);
        }
    }
    None
}

/// Find the last occurrence of a byte pattern.
fn rfind_pattern(data: &[u8], pattern: &[u8]) -> Option<usize> {
    if pattern.is_empty() || data.len() < pattern.len() {
        return None;
    }
    for i in (0..=data.len() - pattern.len()).rev() {
        if &data[i..i + pattern.len()] == pattern {
            return Some(i);
        }
    }
    None
}

/// Extract an integer immediately after `pos`, skipping whitespace.
fn extract_int_after(data: &[u8], pos: usize) -> Option<i32> {
    let mut i = pos;
    while i < data.len() && data[i].is_ascii_whitespace() {
        i += 1;
    }
    let negative = if i < data.len() && data[i] == b'-' {
        i += 1;
        true
    } else {
        false
    };
    let start = i;
    while i < data.len() && data[i].is_ascii_digit() {
        i += 1;
    }
    if i > start {
        if let Ok(s) = std::str::from_utf8(&data[start..i]) {
            if let Ok(n) = s.parse::<i32>() {
                return Some(if negative { -n } else { n });
            }
        }
    }
    None
}

/// Extract a float immediately after `pos`, returning (value, end_pos).
fn extract_float_after(data: &[u8], pos: usize) -> Option<(f32, usize)> {
    let mut i = pos;
    while i < data.len() && data[i].is_ascii_whitespace() {
        i += 1;
    }
    let start = i;
    if i < data.len() && (data[i] == b'-' || data[i] == b'+') {
        i += 1;
    }
    while i < data.len() && data[i].is_ascii_digit() {
        i += 1;
    }
    if i < data.len() && data[i] == b'.' {
        i += 1;
        while i < data.len() && data[i].is_ascii_digit() {
            i += 1;
        }
    }
    if i > start {
        if let Ok(s) = std::str::from_utf8(&data[start..i]) {
            if let Ok(f) = s.parse::<f32>() {
                return Some((f, i));
            }
        }
    }
    None
}

/// Extract a PDF literal string starting at an opening '('.
fn extract_pdf_string(data: &[u8], start: usize) -> Option<String> {
    if start >= data.len() || data[start] != b'(' {
        return None;
    }
    let mut depth = 0i32;
    let mut result = Vec::new();
    let mut i = start;
    while i < data.len() {
        match data[i] {
            b'(' => {
                depth += 1;
                if depth > 1 {
                    result.push(b'(');
                }
            }
            b')' => {
                depth -= 1;
                if depth == 0 {
                    return String::from_utf8(result).ok();
                }
                result.push(b')');
            }
            b'\\' if i + 1 < data.len() => {
                i += 1;
                match data[i] {
                    b'n' => result.push(b'\n'),
                    b'r' => result.push(b'\r'),
                    b't' => result.push(b'\t'),
                    b'(' => result.push(b'('),
                    b')' => result.push(b')'),
                    b'\\' => result.push(b'\\'),
                    c if c.is_ascii_digit() => {
                        let mut octal = (c - b'0') as u32;
                        if i + 1 < data.len() && data[i + 1].is_ascii_digit() {
                            i += 1;
                            octal = octal * 8 + (data[i] - b'0') as u32;
                            if i + 1 < data.len() && data[i + 1].is_ascii_digit() {
                                i += 1;
                                octal = octal * 8 + (data[i] - b'0') as u32;
                            }
                        }
                        result.push(octal as u8);
                    }
                    _ => result.push(data[i]),
                }
            }
            c => result.push(c),
        }
        i += 1;
    }
    None
}

/// Extract a PDF hex string starting at '<' (not '<<').
fn extract_pdf_hex_string(data: &[u8], start: usize) -> Option<String> {
    if start >= data.len() || data[start] != b'<' {
        return None;
    }
    if start + 1 < data.len() && data[start + 1] == b'<' {
        return None;
    }
    let mut i = start + 1;
    let mut hex = Vec::new();
    while i < data.len() && data[i] != b'>' {
        if data[i].is_ascii_hexdigit() {
            hex.push(data[i]);
        }
        i += 1;
    }
    if hex.len() % 2 != 0 {
        hex.push(b'0');
    }
    let mut bytes = Vec::new();
    for pair in hex.chunks(2) {
        if let Ok(s) = std::str::from_utf8(pair) {
            if let Ok(b) = u8::from_str_radix(s, 16) {
                bytes.push(b);
            }
        }
    }
    String::from_utf8(bytes).ok()
}

/// Find a named key in a dictionary region, returning the offset right
/// after the key name (where the value begins).
fn find_dict_key(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_pattern(region, key).map(|pos| region_start + pos + key.len())
}

/// Find the matching '>>' for a '<<' at `start`.
fn find_dict_end(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 the trailer dictionary region (start_of_<<, end_after_>>).
fn find_trailer_region(data: &[u8]) -> Option<(usize, usize)> {
    let trailer_pos = rfind_pattern(data, b"trailer")?;
    let after = &data[trailer_pos..];
    let dict_start_rel = find_pattern(after, b"<<")?;
    let dict_start = trailer_pos + dict_start_rel;
    let dict_end = find_dict_end(data, dict_start)?;
    Some((dict_start, dict_end + 2))
}

/// Find an indirect object's dictionary region: "N 0 obj << ... >>".
///
/// Ensures the object number match is at a word boundary (preceded by
/// a newline, carriage return, or start of data) so that e.g. searching
/// for "1 0 obj" doesn't match inside "21 0 obj".
fn find_object_dict(data: &[u8], obj_num: i32) -> Option<(usize, usize)> {
    let pattern = format!("{} 0 obj", obj_num);
    let pat_bytes = pattern.as_bytes();
    let positions = find_all_patterns(data, pat_bytes);
    for &pos in positions.iter().rev() {
        if pos > 0 {
            let prev = data[pos - 1];
            if prev != b'\n' && prev != b'\r' && prev != b' ' {
                continue;
            }
        }
        let after = &data[pos..];
        if let Some(dict_rel) = find_pattern(after, b"<<") {
            let dict_start = pos + dict_rel;
            if let Some(dict_end) = find_dict_end(data, dict_start) {
                return Some((dict_start, dict_end + 2));
            }
        }
    }
    None
}

/// Resolve an indirect reference "N G R" starting at `pos`.
/// Returns N regardless of whether a full "N G R" token is present
/// (also works for a bare integer value).
fn resolve_indirect_ref(data: &[u8], pos: usize) -> Option<i32> {
    extract_int_after(data, pos)
}

/// Extract a string value (literal or hex) from the position after a key.
fn extract_string_value(data: &[u8], pos: usize) -> Option<String> {
    let mut i = pos;
    while i < data.len() && data[i].is_ascii_whitespace() {
        i += 1;
    }
    if i >= data.len() {
        return None;
    }
    match data[i] {
        b'(' => extract_pdf_string(data, i),
        b'<' if i + 1 < data.len() && data[i + 1] != b'<' => extract_pdf_hex_string(data, i),
        _ => None,
    }
}

/// Parse a rectangle array "[x0 y0 x1 y1]" starting at `pos`.
fn parse_rect_array(data: &[u8], pos: usize) -> Option<[f32; 4]> {
    let mut i = pos;
    while i < data.len() && data[i].is_ascii_whitespace() {
        i += 1;
    }
    if i >= data.len() || data[i] != b'[' {
        return None;
    }
    i += 1;
    let mut values = [0.0f32; 4];
    for val in &mut values {
        if let Some((f, next_i)) = extract_float_after(data, i) {
            *val = f;
            i = next_i;
        } else {
            return None;
        }
    }
    Some(values)
}

/// Find the /Info dictionary object number from the trailer or XRef stream.
fn find_info_obj_num(data: &[u8]) -> Option<i32> {
    if let Some((ts, te)) = find_trailer_region(data) {
        if let Some(kp) = find_dict_key(data, ts, te, b"/Info") {
            if let Some(num) = resolve_indirect_ref(data, kp) {
                return Some(num);
            }
        }
    }
    let (ts, te) = find_xref_stream_dict(data)?;
    let kp = find_dict_key(data, ts, te, b"/Info")?;
    resolve_indirect_ref(data, kp)
}

/// Find the /Root (Catalog) object number from the trailer or XRef stream.
fn find_root_obj_num(data: &[u8]) -> Option<i32> {
    if let Some((ts, te)) = find_trailer_region(data) {
        if let Some(kp) = find_dict_key(data, ts, te, b"/Root") {
            if let Some(num) = resolve_indirect_ref(data, kp) {
                return Some(num);
            }
        }
    }
    find_root_from_xref_stream(data)
}

/// Find the dictionary region of the last XRef stream in the PDF.
///
/// PDFs using cross-reference streams embed the trailer entries in the
/// XRef stream object dictionary (`/Type /XRef`).  Returns the
/// (dict_start, dict_end) of that dictionary.
fn find_xref_stream_dict(data: &[u8]) -> Option<(usize, usize)> {
    let xref_patterns: &[&[u8]] = &[b"/Type/XRef", b"/Type /XRef"];
    let mut best_pos: Option<usize> = None;
    for pat in xref_patterns {
        let positions = find_all_patterns(data, pat);
        if let Some(&last) = positions.last() {
            best_pos = Some(best_pos.map_or(last, |prev: usize| prev.max(last)));
        }
    }
    let xref_pos = best_pos?;

    let search_start = xref_pos.saturating_sub(500);
    let before = &data[search_start..xref_pos];
    let obj_rel = rfind_pattern(before, b" obj")?;
    let obj_abs = search_start + obj_rel;

    let dict_start = find_pattern(&data[obj_abs..], b"<<")?;
    let dict_abs = obj_abs + dict_start;
    let dict_end = find_dict_end(data, dict_abs)?;

    Some((dict_abs, dict_end + 2))
}

/// Find the /Root reference from an XRef stream dictionary.
fn find_root_from_xref_stream(data: &[u8]) -> Option<i32> {
    let (ts, te) = find_xref_stream_dict(data)?;
    let kp = find_dict_key(data, ts, te, b"/Root")?;
    resolve_indirect_ref(data, kp)
}

/// Find the /Encrypt dictionary object number from the trailer or XRef stream.
fn find_encrypt_obj_num(data: &[u8]) -> Option<i32> {
    if let Some((ts, te)) = find_trailer_region(data) {
        if let Some(kp) = find_dict_key(data, ts, te, b"/Encrypt") {
            if let Some(num) = resolve_indirect_ref(data, kp) {
                return Some(num);
            }
        }
    }
    let (ts, te) = find_xref_stream_dict(data)?;
    let kp = find_dict_key(data, ts, te, b"/Encrypt")?;
    resolve_indirect_ref(data, kp)
}

/// Read the /P (permissions) integer from the /Encrypt dictionary.
fn get_permissions_value(data: &[u8]) -> Option<i32> {
    let encrypt_num = find_encrypt_obj_num(data)?;
    let (ds, de) = find_object_dict(data, encrypt_num)?;
    let kp = find_dict_key(data, ds, de, b"/P")?;
    extract_int_after(data, kp)
}

/// Look up a metadata value from the /Info dictionary.
fn lookup_info_metadata(data: &[u8], key: &str) -> Option<String> {
    let info_num = find_info_obj_num(data)?;
    let (ds, de) = find_object_dict(data, info_num)?;
    let pdf_key = format!("/{}", key);
    let kp = find_dict_key(data, ds, de, pdf_key.as_bytes())?;
    extract_string_value(data, kp)
}

/// Find the Nth page object dictionary (0-indexed).
///
/// Handles both `/Type /Page` and `/Type/Page` (with or without space).
fn find_page_dict(data: &[u8], page_num: i32) -> Option<(usize, usize)> {
    let mut page_dicts: Vec<(usize, usize, usize)> = Vec::new();

    for pattern in [&b"/Type /Page"[..], &b"/Type/Page"[..]] {
        let mut i = 0;
        while i + pattern.len() <= data.len() {
            if &data[i..i + pattern.len()] == pattern {
                let after_idx = i + pattern.len();
                let is_pages =
                    data.get(after_idx) == Some(&b's') || data.get(after_idx) == Some(&b'S');
                if !is_pages {
                    let search_start = i.saturating_sub(500);
                    let before = &data[search_start..i];
                    if let Some(obj_rel) = rfind_pattern(before, b" obj") {
                        let obj_abs = search_start + obj_rel;
                        let after_obj = &data[obj_abs..];
                        if let Some(dict_rel) = find_pattern(after_obj, b"<<") {
                            let dict_start = obj_abs + dict_rel;
                            if let Some(dict_end) = find_dict_end(data, dict_start) {
                                page_dicts.push((i, dict_start, dict_end + 2));
                            }
                        }
                    }
                }
            }
            i += 1;
        }
    }

    page_dicts.sort_by_key(|&(pos, _, _)| pos);
    page_dicts.dedup_by_key(|entry| entry.0);

    page_dicts
        .get(page_num as usize)
        .map(|&(_, ds, de)| (ds, de))
}

/// Parse a page box from a page dictionary, falling back to parent or
/// MediaBox.
fn parse_page_box(
    data: &[u8],
    dict_start: usize,
    dict_end: usize,
    box_name: &[u8],
) -> Option<[f32; 4]> {
    if let Some(pos) = find_dict_key(data, dict_start, dict_end, box_name) {
        if let Some(rect) = parse_rect_array(data, pos) {
            return Some(rect);
        }
    }
    if box_name != b"/MediaBox" {
        if let Some(pos) = find_dict_key(data, dict_start, dict_end, b"/MediaBox") {
            if let Some(rect) = parse_rect_array(data, pos) {
                return Some(rect);
            }
        }
    }
    // Try parent
    if let Some(parent_pos) = find_dict_key(data, dict_start, dict_end, b"/Parent") {
        if let Some(parent_num) = resolve_indirect_ref(data, parent_pos) {
            if let Some((ps, pe)) = find_object_dict(data, parent_num) {
                if let Some(pos) = find_dict_key(data, ps, pe, box_name) {
                    if let Some(rect) = parse_rect_array(data, pos) {
                        return Some(rect);
                    }
                }
                if box_name != b"/MediaBox" {
                    if let Some(pos) = find_dict_key(data, ps, pe, b"/MediaBox") {
                        if let Some(rect) = parse_rect_array(data, pos) {
                            return Some(rect);
                        }
                    }
                }
            }
        }
    }
    None
}

/// Compute the page label for a 0-indexed page from /PageLabels.
fn compute_page_label(data: &[u8], page_num: i32) -> Option<String> {
    let root_num = find_root_obj_num(data)?;
    let (rs, re) = find_object_dict(data, root_num)?;
    let kp = find_dict_key(data, rs, re, b"/PageLabels")?;
    let labels_num = resolve_indirect_ref(data, kp)?;
    let (ls, le) = find_object_dict(data, labels_num)?;
    let nums_pos = find_dict_key(data, ls, le, b"/Nums")?;
    let mut i = nums_pos;
    while i < data.len() && data[i].is_ascii_whitespace() {
        i += 1;
    }
    if i >= data.len() || data[i] != b'[' {
        return None;
    }
    i += 1;

    // Collect (start_page, style, prefix, start_value) ranges.
    let mut ranges: Vec<(i32, Option<String>, Option<String>, i32)> = Vec::new();

    while i < le.min(data.len()) {
        while i < data.len() && data[i].is_ascii_whitespace() {
            i += 1;
        }
        if i >= data.len() || data[i] == b']' {
            break;
        }
        let page_idx = if let Some((f, ni)) = extract_float_after(data, i) {
            i = ni;
            f as i32
        } else {
            break;
        };

        while i < data.len() && data[i].is_ascii_whitespace() {
            i += 1;
        }
        if i >= data.len() {
            break;
        }

        let mut style: Option<String> = None;
        let mut prefix: Option<String> = None;
        let mut start_val: i32 = 1;

        if data[i] == b'<' && i + 1 < data.len() && data[i + 1] == b'<' {
            if let Some(de) = find_dict_end(data, i) {
                let dre = de + 2;
                if let Some(sp) = find_dict_key(data, i, dre, b"/S") {
                    let mut j = sp;
                    while j < dre && data[j].is_ascii_whitespace() {
                        j += 1;
                    }
                    if j < dre && data[j] == b'/' {
                        j += 1;
                        let ss = j;
                        while j < dre
                            && !data[j].is_ascii_whitespace()
                            && data[j] != b'/'
                            && data[j] != b'>'
                        {
                            j += 1;
                        }
                        style = std::str::from_utf8(&data[ss..j])
                            .ok()
                            .map(|s| s.to_string());
                    }
                }
                if let Some(pp) = find_dict_key(data, i, dre, b"/P") {
                    prefix = extract_string_value(data, pp);
                }
                if let Some(stp) = find_dict_key(data, i, dre, b"/St") {
                    if let Some(v) = extract_int_after(data, stp) {
                        start_val = v;
                    }
                }
                i = dre;
            } else {
                break;
            }
        } else if data[i].is_ascii_digit() {
            if let Some(ref_num) = resolve_indirect_ref(data, i) {
                while i < data.len() && data[i].is_ascii_whitespace() {
                    i += 1;
                }
                while i < data.len() && data[i].is_ascii_digit() {
                    i += 1;
                }
                while i < data.len() && data[i].is_ascii_whitespace() {
                    i += 1;
                }
                while i < data.len() && data[i].is_ascii_digit() {
                    i += 1;
                }
                while i < data.len() && data[i].is_ascii_whitespace() {
                    i += 1;
                }
                if i < data.len() && data[i] == b'R' {
                    i += 1;
                }
                if let Some((ds2, de2)) = find_object_dict(data, ref_num) {
                    if let Some(sp) = find_dict_key(data, ds2, de2, b"/S") {
                        let mut j = sp;
                        while j < de2 && data[j].is_ascii_whitespace() {
                            j += 1;
                        }
                        if j < de2 && data[j] == b'/' {
                            j += 1;
                            let ss = j;
                            while j < de2
                                && !data[j].is_ascii_whitespace()
                                && data[j] != b'/'
                                && data[j] != b'>'
                            {
                                j += 1;
                            }
                            style = std::str::from_utf8(&data[ss..j])
                                .ok()
                                .map(|s| s.to_string());
                        }
                    }
                    if let Some(pp) = find_dict_key(data, ds2, de2, b"/P") {
                        prefix = extract_string_value(data, pp);
                    }
                    if let Some(stp) = find_dict_key(data, ds2, de2, b"/St") {
                        if let Some(v) = extract_int_after(data, stp) {
                            start_val = v;
                        }
                    }
                }
            } else {
                break;
            }
        } else {
            i += 1;
            continue;
        }
        ranges.push((page_idx, style, prefix, start_val));
    }

    if ranges.is_empty() {
        return None;
    }
    ranges.sort_by_key(|r| r.0);

    let mut applicable = &ranges[0];
    for range in &ranges {
        if range.0 <= page_num {
            applicable = range;
        } else {
            break;
        }
    }
    let offset = page_num - applicable.0;
    let value = applicable.3 + offset;
    let prefix_str = applicable.2.as_deref().unwrap_or("");
    let number_str = match applicable.1.as_deref() {
        Some("D") => format!("{}", value),
        Some("r") => to_roman(value).to_lowercase(),
        Some("R") => to_roman(value),
        Some("A") => to_alpha_upper(value),
        Some("a") => to_alpha_lower(value),
        None => String::new(),
        Some(_) => format!("{}", value),
    };
    Some(format!("{}{}", prefix_str, number_str))
}

/// Convert integer to uppercase Roman numeral.
fn to_roman(mut n: i32) -> String {
    if n <= 0 || n > 4999 {
        return format!("{}", n);
    }
    let table = [
        (1000, "M"),
        (900, "CM"),
        (500, "D"),
        (400, "CD"),
        (100, "C"),
        (90, "XC"),
        (50, "L"),
        (40, "XL"),
        (10, "X"),
        (9, "IX"),
        (5, "V"),
        (4, "IV"),
        (1, "I"),
    ];
    let mut result = String::new();
    for &(val, sym) in &table {
        while n >= val {
            result.push_str(sym);
            n -= val;
        }
    }
    result
}

/// Convert integer to uppercase letter label (1=A, 26=Z, 27=AA, ...).
fn to_alpha_upper(n: i32) -> String {
    if n <= 0 {
        return String::new();
    }
    let mut result = String::new();
    let mut val = n - 1;
    loop {
        result.insert(0, (b'A' + (val % 26) as u8) as char);
        val = val / 26 - 1;
        if val < 0 {
            break;
        }
    }
    result
}

fn to_alpha_lower(n: i32) -> String {
    to_alpha_upper(n).to_lowercase()
}

/// Build outline tree from PDF /Outlines dictionary.
fn build_outline_from_pdf(data: &[u8]) -> Option<Handle> {
    let root_num = find_root_obj_num(data)?;
    let (rs, re) = find_object_dict(data, root_num)?;
    let outlines_key = find_dict_key(data, rs, re, b"/Outlines")?;
    let outlines_num = resolve_indirect_ref(data, outlines_key)?;
    let (os, oe) = find_object_dict(data, outlines_num)?;
    let first_key = find_dict_key(data, os, oe, b"/First")?;
    let first_num = resolve_indirect_ref(data, first_key)?;
    build_outline_item(data, first_num)
}

/// Recursively build an outline item and its siblings/children.
fn build_outline_item(data: &[u8], obj_num: i32) -> Option<Handle> {
    let (ds, de) = find_object_dict(data, obj_num)?;
    let title = find_dict_key(data, ds, de, b"/Title").and_then(|p| extract_string_value(data, p));
    let page_num = extract_outline_page_dest(data, ds, de);

    let is_open = find_dict_key(data, ds, de, b"/Count")
        .and_then(|p| extract_int_after(data, p))
        .map(|c| c > 0)
        .unwrap_or(false);

    let mut outline = super::outline::Outline::default();
    outline.title = title;
    outline.uri = page_num.map(|p| format!("#page={}", p));
    outline.page = super::outline::Location {
        chapter: 0,
        page: page_num.unwrap_or(-1),
    };
    outline.is_open = is_open;

    if let Some(fk) = find_dict_key(data, ds, de, b"/First") {
        if let Some(fn_) = resolve_indirect_ref(data, fk) {
            outline.down = build_outline_item(data, fn_);
        }
    }

    let handle = OUTLINES.insert(outline);

    if let Some(nk) = find_dict_key(data, ds, de, b"/Next") {
        if let Some(nn) = resolve_indirect_ref(data, nk) {
            if let Some(next_handle) = build_outline_item(data, nn) {
                if let Some(arc) = OUTLINES.get(handle) {
                    if let Ok(mut o) = arc.lock() {
                        o.next = Some(next_handle);
                    }
                }
            }
        }
    }

    Some(handle)
}

/// Extract destination page number from an outline item.
fn extract_outline_page_dest(data: &[u8], ds: usize, de: usize) -> Option<i32> {
    if let Some(dest_pos) = find_dict_key(data, ds, de, b"/Dest") {
        let mut i = dest_pos;
        while i < data.len() && data[i].is_ascii_whitespace() {
            i += 1;
        }
        if i < data.len() && data[i] == b'[' {
            i += 1;
            if let Some(page_ref) = resolve_indirect_ref(data, i) {
                return page_obj_to_index(data, page_ref);
            }
        }
    }
    if let Some(a_pos) = find_dict_key(data, ds, de, b"/A") {
        let mut i = a_pos;
        while i < data.len() && data[i].is_ascii_whitespace() {
            i += 1;
        }
        let (as_, ae_) =
            if i < data.len() && data[i] == b'<' && i + 1 < data.len() && data[i + 1] == b'<' {
                if let Some(end) = find_dict_end(data, i) {
                    (i, end + 2)
                } else {
                    return None;
                }
            } else if let Some(rn) = resolve_indirect_ref(data, i) {
                if let Some(r) = find_object_dict(data, rn) {
                    r
                } else {
                    return None;
                }
            } else {
                return None;
            };
        if let Some(d_pos) = find_dict_key(data, as_, ae_, b"/D") {
            let mut j = d_pos;
            while j < data.len() && data[j].is_ascii_whitespace() {
                j += 1;
            }
            if j < data.len() && data[j] == b'[' {
                j += 1;
                if let Some(page_ref) = resolve_indirect_ref(data, j) {
                    return page_obj_to_index(data, page_ref);
                }
            }
        }
    }
    None
}

/// Convert a page object number to a 0-based page index.
///
/// Walks the /Pages tree /Kids arrays to determine the canonical page
/// order rather than relying on file position.  Falls back to a linear
/// scan of `/Type/Page` entries if the tree walk fails.
fn page_obj_to_index(data: &[u8], page_obj_num: i32) -> Option<i32> {
    if let Some(idx) = page_obj_to_index_via_kids(data, page_obj_num) {
        return Some(idx);
    }
    page_obj_to_index_by_scan(data, page_obj_num)
}

/// Walk the /Pages /Kids tree to map page obj number to index.
fn page_obj_to_index_via_kids(data: &[u8], page_obj_num: i32) -> Option<i32> {
    let root_num = find_root_obj_num(data)?;
    let (rs, re) = find_object_dict(data, root_num)?;
    let pages_pos = find_dict_key(data, rs, re, b"/Pages")?;
    let pages_num = resolve_indirect_ref(data, pages_pos)?;
    let mut leaf_pages = Vec::new();
    collect_page_leaves(data, pages_num, &mut leaf_pages);
    for (idx, &num) in leaf_pages.iter().enumerate() {
        if num == page_obj_num {
            return Some(idx as i32);
        }
    }
    None
}

/// Recursively collect leaf page object numbers from a /Pages tree node.
fn collect_page_leaves(data: &[u8], obj_num: i32, pages: &mut Vec<i32>) {
    let Some((ds, de)) = find_object_dict(data, obj_num) else {
        return;
    };
    let region = &data[ds..de.min(data.len())];
    let has_kids = find_pattern(region, b"/Kids").is_some();
    let is_page = find_pattern(region, b"/Type/Page").is_some()
        || find_pattern(region, b"/Type /Page").is_some();
    let is_pages = find_pattern(region, b"/Type/Pages").is_some()
        || find_pattern(region, b"/Type /Pages").is_some();

    if is_pages || has_kids {
        if let Some(kids_pos) = find_dict_key(data, ds, de, b"/Kids") {
            let mut i = kids_pos;
            while i < data.len() && data[i].is_ascii_whitespace() {
                i += 1;
            }
            if i < data.len() && data[i] == b'[' {
                i += 1;
                while i < data.len() && data[i] != b']' {
                    while i < data.len() && data[i].is_ascii_whitespace() {
                        i += 1;
                    }
                    if i < data.len() && data[i] == b']' {
                        break;
                    }
                    if let Some(child_num) = resolve_indirect_ref(data, i) {
                        collect_page_leaves(data, child_num, pages);
                    }
                    while i < data.len()
                        && (data[i].is_ascii_digit() || data[i].is_ascii_whitespace())
                    {
                        i += 1;
                    }
                    if i < data.len() && data[i] == b'R' {
                        i += 1;
                    }
                }
            }
        }
    } else if is_page {
        pages.push(obj_num);
    }
}

/// Fallback: scan for /Type/Page entries by file position.
fn page_obj_to_index_by_scan(data: &[u8], page_obj_num: i32) -> Option<i32> {
    let patterns: &[&[u8]] = &[b"/Type /Page", b"/Type/Page"];
    let mut page_positions: Vec<(usize, i32)> = Vec::new();

    for &pattern in patterns {
        let mut i = 0;
        while i + pattern.len() <= data.len() {
            if &data[i..i + pattern.len()] == pattern {
                let after_idx = i + pattern.len();
                let is_pages =
                    data.get(after_idx) == Some(&b's') || data.get(after_idx) == Some(&b'S');
                if !is_pages {
                    if let Some(num) = extract_obj_num_before(data, i) {
                        page_positions.push((i, num));
                    }
                }
            }
            i += 1;
        }
    }

    page_positions.sort_by_key(|&(pos, _)| pos);
    page_positions.dedup_by_key(|entry| entry.0);

    for (page_index, &(_, num)) in page_positions.iter().enumerate() {
        if num == page_obj_num {
            return Some(page_index as i32);
        }
    }
    None
}

/// Extract the object number from the "N G obj" header before a position.
fn extract_obj_num_before(data: &[u8], pos: usize) -> Option<i32> {
    let search_start = pos.saturating_sub(500);
    let before = &data[search_start..pos];
    let obj_pos = rfind_pattern(before, b" obj")?;
    let region = &data[search_start..search_start + obj_pos];
    let line_start = rfind_pattern(region, b"\n")?;
    let num_region = &data[search_start + line_start + 1..search_start + obj_pos];
    let num_str = String::from_utf8_lossy(num_region);
    num_str
        .trim()
        .split_whitespace()
        .next()?
        .parse::<i32>()
        .ok()
}

/// Extract the /Contents stream data from a page dictionary.
fn extract_page_contents(
    data: &[u8],
    page_dict_start: usize,
    page_dict_end: usize,
) -> Option<Vec<u8>> {
    let contents_pos = find_dict_key(data, page_dict_start, page_dict_end, b"/Contents")?;
    let mut i = contents_pos;
    while i < data.len() && data[i].is_ascii_whitespace() {
        i += 1;
    }

    if i < data.len() && data[i] == b'[' {
        i += 1;
        let mut combined = Vec::new();
        while i < data.len() && data[i] != b']' {
            while i < data.len() && data[i].is_ascii_whitespace() {
                i += 1;
            }
            if i < data.len() && data[i] == b']' {
                break;
            }
            if let Some(rn) = resolve_indirect_ref(data, i) {
                if let Some(sd) = extract_stream_data(data, rn) {
                    combined.extend_from_slice(&sd);
                    combined.push(b'\n');
                }
            }
            while i < data.len() && data[i].is_ascii_whitespace() {
                i += 1;
            }
            while i < data.len() && data[i].is_ascii_digit() {
                i += 1;
            }
            while i < data.len() && data[i].is_ascii_whitespace() {
                i += 1;
            }
            while i < data.len() && data[i].is_ascii_digit() {
                i += 1;
            }
            while i < data.len() && data[i].is_ascii_whitespace() {
                i += 1;
            }
            if i < data.len() && data[i] == b'R' {
                i += 1;
            }
        }
        if combined.is_empty() {
            None
        } else {
            Some(combined)
        }
    } else {
        let rn = resolve_indirect_ref(data, i)?;
        extract_stream_data(data, rn)
    }
}

/// Extract raw stream data from an object (uncompressed).
fn extract_stream_data(data: &[u8], obj_num: i32) -> Option<Vec<u8>> {
    let pattern = format!("{} 0 obj", obj_num);
    let obj_pos = rfind_pattern(data, pattern.as_bytes())?;
    let after = &data[obj_pos..];
    let stream_kw = find_pattern(after, b"stream")?;
    let mut stream_start = obj_pos + stream_kw + 6;
    if stream_start < data.len() && data[stream_start] == b'\r' {
        stream_start += 1;
    }
    if stream_start < data.len() && data[stream_start] == b'\n' {
        stream_start += 1;
    }
    let endstream_pos = find_pattern(&data[stream_start..], b"endstream")?;
    let mut stream_end = stream_start + endstream_pos;
    while stream_end > stream_start && data[stream_end - 1].is_ascii_whitespace() {
        stream_end -= 1;
    }
    Some(data[stream_start..stream_end].to_vec())
}

/// Write a metadata value into an output buffer, returning byte count.
fn write_metadata_value(value: &str, buf: *mut c_char, size: i32) -> i32 {
    if buf.is_null() || size <= 0 {
        return value.len() as i32;
    }
    let bytes = value.as_bytes();
    let copy_len = bytes.len().min((size - 1) as usize);
    unsafe {
        std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
        *buf.add(copy_len) = 0;
    }
    copy_len as i32
}

/// Page storage
pub static PAGES: LazyLock<HandleStore<Page>> = LazyLock::new(HandleStore::default);

/// Internal page state
pub struct Page {
    pub doc_handle: Handle,
    pub page_num: i32,
    pub bounds: [f32; 4],         // x0, y0, x1, y1
    pub annotations: Vec<Handle>, // List of annotation handles on this page
    pub widgets: Vec<Handle>,     // List of form field widget handles on this page
}

impl Page {
    pub fn new(doc_handle: Handle, page_num: i32) -> Self {
        let bounds =
            Self::read_page_bounds(doc_handle, page_num).unwrap_or([0.0, 0.0, 612.0, 792.0]);

        Self {
            doc_handle,
            page_num,
            bounds,
            annotations: Vec::new(),
            widgets: Vec::new(),
        }
    }

    /// Read the MediaBox from the document's PDF data for the given page.
    fn read_page_bounds(doc_handle: Handle, page_num: i32) -> Option<[f32; 4]> {
        let doc_arc = DOCUMENTS.get(doc_handle)?;
        let guard = doc_arc.lock().ok()?;
        let data = guard.data();
        let (ds, de) = find_page_dict(data, page_num)?;
        parse_page_box(data, ds, de, b"/MediaBox")
    }

    /// Create a page with explicit bounds (e.g. from /MediaBox)
    pub fn with_bounds(doc_handle: Handle, page_num: i32, bounds: [f32; 4]) -> Self {
        Self {
            doc_handle,
            page_num,
            bounds,
            annotations: Vec::new(),
            widgets: Vec::new(),
        }
    }

    /// Add an annotation to this page
    pub fn add_annotation(&mut self, annot_handle: Handle) {
        if !self.annotations.contains(&annot_handle) {
            self.annotations.push(annot_handle);
        }
    }

    /// Remove an annotation from this page
    pub fn remove_annotation(&mut self, annot_handle: Handle) {
        self.annotations.retain(|&h| h != annot_handle);
    }

    /// Get first annotation handle
    pub fn first_annotation(&self) -> Option<Handle> {
        self.annotations.first().copied()
    }

    /// Get next annotation after given handle
    pub fn next_annotation(&self, current: Handle) -> Option<Handle> {
        if let Some(pos) = self.annotations.iter().position(|&h| h == current) {
            self.annotations.get(pos + 1).copied()
        } else {
            None
        }
    }

    /// Add a widget to this page
    pub fn add_widget(&mut self, widget_handle: Handle) {
        if !self.widgets.contains(&widget_handle) {
            self.widgets.push(widget_handle);
        }
    }

    /// Remove a widget from this page
    pub fn remove_widget(&mut self, widget_handle: Handle) {
        self.widgets.retain(|&h| h != widget_handle);
    }

    /// Get first widget handle
    pub fn first_widget(&self) -> Option<Handle> {
        self.widgets.first().copied()
    }

    /// Get next widget after given handle
    pub fn next_widget(&self, current: Handle) -> Option<Handle> {
        if let Some(pos) = self.widgets.iter().position(|&h| h == current) {
            self.widgets.get(pos + 1).copied()
        } else {
            None
        }
    }
}

/// Internal document state
pub struct Document {
    // PDF document data - will be expanded with actual PDF parsing
    #[allow(dead_code)]
    data: Vec<u8>,
    pub(crate) page_count: i32,
    needs_password: bool,
    authenticated: bool,
    password: Option<String>,
    pub format: String,
}

impl Document {
    pub fn new(data: Vec<u8>) -> Self {
        // Scan the raw PDF bytes to count /Type /Page markers and
        // detect the document format from the header magic.
        let page_count = Self::estimate_page_count(&data);

        // Detect format from magic bytes
        let format = if data.starts_with(b"%PDF-") {
            "PDF".to_string()
        } else if data.starts_with(b"<?xml") {
            "XML".to_string()
        } else {
            "Unknown".to_string()
        };

        Self {
            data,
            page_count,
            needs_password: false,
            authenticated: true,
            password: None,
            format,
        }
    }

    fn estimate_page_count(data: &[u8]) -> i32 {
        // Method 1: Robust trailer/xref parsing - works for compressed, linearized PDFs
        // Skip when result is <= 1 since Catalog/Pages may be in ObjStm (PDF v2.0)
        if let Some(count) = Self::proper_page_count(data) {
            if count > 1 {
                return count;
            }
        }

        // Method 2: Try object streams (PDF v2.0 and compressed PDFs store page tree here)
        // Critical for PDF v2.0 where Catalog and /Pages are inside ObjStm
        if let Some(objstm_count) = get_page_count_from_object_streams(data) {
            if objstm_count > 0 {
                return objstm_count;
            }
        }

        // Method 3: Look for /Count N in /Type /Pages dictionary (heuristic fallback)
        if let Some(count) = Self::find_pages_count(data) {
            if count > 0 {
                return count;
            }
        }

        // Method 4: Count /Type /Page and /Type/Page patterns (for uncompressed PDFs)
        let mut count = 0;
        let page_patterns: &[&[u8]] = &[b"/Type /Page", b"/Type/Page"];

        for pattern in page_patterns {
            for i in 0..data.len().saturating_sub(pattern.len()) {
                if &data[i..i + pattern.len()] == *pattern {
                    let next_byte = data.get(i + pattern.len());
                    if next_byte != Some(&b's') {
                        count += 1;
                    }
                }
            }
        }

        if count > 0 {
            return count;
        }

        1 // Fallback to at least 1 page
    }

    /// Robust page count from PDF trailer/xref structure.
    /// Works for compressed, linearized PDFs where heuristics fail.
    fn proper_page_count(data: &[u8]) -> Option<i32> {
        // 1. Find startxref - search backwards from end (last ~2KB)
        let search_start = data.len().saturating_sub(2048);
        let search_region = &data[search_start..];
        let startxref_pos = Self::find_last_pattern(search_region, b"startxref")?;
        let abs_startxref = search_start + startxref_pos;

        // 2. Parse the offset after startxref
        let after_startxref = &data[abs_startxref + 9..]; // "startxref" is 9 chars
        let xref_offset = Self::parse_trailing_number(after_startxref)?;

        // 3. Get /Root from trailer (or from xref stream dict)
        let root_ref = if xref_offset < data.len() {
            let at_offset = &data[xref_offset..];
            if at_offset.starts_with(b"xref") {
                Self::get_root_from_trailer(data, abs_startxref)?
            } else if at_offset.starts_with(b"0 ") || at_offset.starts_with(b"1 ") {
                // Some PDFs point startxref to first xref entry; trailer is before startxref
                Self::get_root_from_trailer(data, abs_startxref)?
            } else {
                Self::get_root_from_xref_stream(data, xref_offset)?
            }
        } else {
            Self::get_root_from_trailer(data, abs_startxref)?
        };

        // 4. Find catalog object
        let catalog_data = Self::find_object_content(data, root_ref.0, root_ref.1)?;

        // 5. Find /Pages reference in catalog
        let pages_ref = Self::extract_ref_from_dict(catalog_data, b"/Pages")?;

        // 6. Find pages object
        let pages_data = Self::find_object_content(data, pages_ref.0, pages_ref.1)?;

        // 7. Read /Count from pages dict
        Self::extract_count_from_dict_bytes(pages_data)
    }

    /// Find last occurrence of pattern in data
    fn find_last_pattern(data: &[u8], pattern: &[u8]) -> Option<usize> {
        let mut last = None;
        let mut i = 0;
        while i + pattern.len() <= data.len() {
            if &data[i..i + pattern.len()] == pattern {
                last = Some(i);
            }
            i += 1;
        }
        last
    }

    /// Parse first number (digits) from start of slice
    fn parse_trailing_number(data: &[u8]) -> Option<usize> {
        let mut i = 0;
        while i < data.len()
            && (data[i] == b' ' || data[i] == b'\t' || data[i] == b'\n' || data[i] == b'\r')
        {
            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()?.parse().ok()
        } else {
            None
        }
    }

    /// Get /Root from trailer dict (trailer is before startxref)
    fn get_root_from_trailer(data: &[u8], startxref_pos: usize) -> Option<(i32, i32)> {
        // Search backwards from startxref for "trailer"
        let search_end = startxref_pos.min(data.len());
        let search_start = search_end.saturating_sub(1024);
        let region = &data[search_start..search_end];
        let trailer_pos = Self::find_last_pattern(region, b"trailer")?;
        let trailer_start = search_start + trailer_pos + 7; // "trailer" is 7 chars
        let dict_start = data[trailer_start..].iter().position(|&b| b == b'<')?;
        let dict_region = &data[trailer_start + dict_start..];
        let dict_content = Self::extract_dict_content(dict_region)?;
        Self::extract_ref_from_dict(dict_content, b"/Root")
    }

    /// Get /Root from xref stream object dict (at offset we have "N M obj << ... >>")
    fn get_root_from_xref_stream(data: &[u8], offset: usize) -> Option<(i32, i32)> {
        if offset >= data.len() {
            return None;
        }
        let region = &data[offset..];
        let dict_start = Self::find_pattern(region, b"<<")?;
        let dict_content = Self::extract_dict_content(&region[dict_start..])?;
        Self::extract_ref_from_dict(dict_content, b"/Root")
    }

    /// Extract content of outermost << ... >> dict, handling nested dicts
    fn extract_dict_content(data: &[u8]) -> Option<&[u8]> {
        let start = data.iter().position(|&b| b == b'<')?;
        if start + 1 >= data.len() || data[start + 1] != b'<' {
            return None;
        }
        let mut depth = 1;
        let mut i = start + 2;
        while i < data.len() && depth > 0 {
            if data[i] == b'<' && i + 1 < data.len() && data[i + 1] == b'<' {
                depth += 1;
                i += 2;
            } else if data[i] == b'>' && i + 1 < data.len() && data[i + 1] == b'>' {
                depth -= 1;
                if depth == 0 {
                    return Some(&data[start + 2..i]);
                }
                i += 2;
            } else {
                i += 1;
            }
        }
        None
    }

    /// Extract object reference (obj_num, gen_num) for key like /Root
    fn extract_ref_from_dict(data: &[u8], key: &[u8]) -> Option<(i32, i32)> {
        let key_with_space = [key, b" "].concat();
        let pos =
            Self::find_pattern(data, &key_with_space).or_else(|| Self::find_pattern(data, key))?;
        let after = &data[pos + key.len()..];
        let mut i = 0;
        while i < after.len()
            && (after[i] == b' ' || after[i] == b'\t' || after[i] == b'\n' || after[i] == b'\r')
        {
            i += 1;
        }
        let num_start = i;
        while i < after.len() && (after[i].is_ascii_digit() || (i == num_start && after[i] == b'-'))
        {
            i += 1;
        }
        let obj_num = std::str::from_utf8(&after[num_start..i])
            .ok()?
            .parse()
            .ok()?;
        while i < after.len()
            && (after[i] == b' ' || after[i] == b'\t' || after[i] == b'\n' || after[i] == b'\r')
        {
            i += 1;
        }
        let gen_start = i;
        while i < after.len() && after[i].is_ascii_digit() {
            i += 1;
        }
        let gen_num = std::str::from_utf8(&after[gen_start..i])
            .ok()?
            .parse()
            .ok()?;
        while i < after.len()
            && (after[i] == b' ' || after[i] == b'\t' || after[i] == b'\n' || after[i] == b'\r')
        {
            i += 1;
        }
        if i < after.len() && after[i] == b'R' {
            Some((obj_num, gen_num))
        } else {
            None
        }
    }

    /// Find first occurrence of pattern
    fn find_pattern(data: &[u8], pattern: &[u8]) -> Option<usize> {
        if data.len() < pattern.len() {
            return None;
        }
        for i in 0..=data.len() - pattern.len() {
            if &data[i..i + pattern.len()] == pattern {
                return Some(i);
            }
        }
        None
    }

    /// Find object by number and return its content (between obj and endobj)
    fn find_object_content(data: &[u8], obj_num: i32, gen_num: i32) -> Option<&[u8]> {
        let pattern = format!("{} {} obj", obj_num, gen_num);
        let pattern_bytes = pattern.as_bytes();
        let mut pos = 0;
        let mut last_found = None;
        while pos < data.len() {
            if let Some(rel) = Self::find_pattern(&data[pos..], pattern_bytes) {
                let abs_pos = pos + rel;
                // Ensure word boundary: character before must be whitespace/newline or start of data
                // to prevent "42 0 obj" matching inside "142 0 obj"
                if abs_pos > 0 {
                    let prev = data[abs_pos - 1];
                    if prev.is_ascii_digit() || prev.is_ascii_alphabetic() {
                        pos = abs_pos + 1;
                        continue;
                    }
                }
                let obj_start = abs_pos + pattern_bytes.len();
                let content_start = data[obj_start..]
                    .iter()
                    .position(|&b| b == b'<')
                    .or_else(|| data[obj_start..].iter().position(|&b| b == b'('))
                    .or_else(|| data[obj_start..].iter().position(|&b| b == b'['))
                    .or_else(|| data[obj_start..].iter().position(|&b| b == b'/'))
                    .map(|p| obj_start + p)
                    .unwrap_or(obj_start);
                if let Some(endobj_pos) = Self::find_pattern(&data[content_start..], b"endobj") {
                    if content_start + endobj_pos > obj_start {
                        last_found = Some(&data[content_start..content_start + endobj_pos]);
                    }
                }
                pos = obj_start + 1;
            } else {
                break;
            }
        }
        last_found
    }

    /// Extract /Count from dict bytes (handles both `/Count 7` and `/Count7`)
    fn extract_count_from_dict_bytes(data: &[u8]) -> Option<i32> {
        let pos = Self::find_pattern(data, b"/Count")?;
        let after = pos + b"/Count".len();
        let mut start = after;
        while start < data.len() && data[start] == b' ' {
            start += 1;
        }
        let mut end = start;
        while end < data.len() && data[end].is_ascii_digit() {
            end += 1;
        }
        if end > start {
            std::str::from_utf8(&data[start..end]).ok()?.parse().ok()
        } else {
            None
        }
    }

    /// Find the /Count value from /Type /Pages dictionary
    fn find_pages_count(data: &[u8]) -> Option<i32> {
        // Look for /Type /Pages or /Type/Pages (compact notation)
        let patterns: &[&[u8]] = &[b"/Type /Pages", b"/Type/Pages"];

        for pages_pattern in patterns {
            for i in 0..data.len().saturating_sub(pages_pattern.len()) {
                if &data[i..i + pages_pattern.len()] == *pages_pattern {
                    // Found /Type /Pages, search the surrounding dict for /Count
                    let dict_start = data[..i]
                        .windows(2)
                        .rposition(|w| w == b"<<")
                        .unwrap_or(i.saturating_sub(200));
                    let search_end = (i + 200).min(data.len());

                    if let Some(count) = Self::extract_count_value(&data[dict_start..search_end]) {
                        return Some(count);
                    }
                }
            }
        }

        None
    }

    /// Extract numeric value after /Count
    fn extract_count_value(data: &[u8]) -> Option<i32> {
        let count_pattern = b"/Count ";

        // Find /Count in the data
        for i in 0..data.len().saturating_sub(count_pattern.len()) {
            if &data[i..i + count_pattern.len()] == count_pattern {
                // Extract the number following /Count
                let start = i + count_pattern.len();
                let mut end = start;

                while end < data.len() && data[end].is_ascii_digit() {
                    end += 1;
                }

                if end > start {
                    if let Ok(num_str) = std::str::from_utf8(&data[start..end]) {
                        if let Ok(count) = num_str.parse::<i32>() {
                            return Some(count);
                        }
                    }
                }
            }
        }

        None
    }

    /// Get document data
    pub fn data(&self) -> &[u8] {
        &self.data
    }

    /// Replace the document's internal PDF data
    pub fn set_data(&mut self, data: Vec<u8>) {
        self.page_count = Self::estimate_page_count(&data);
        self.data = data;
    }

    /// Get MediaBox [x0, y0, x1, y1] for the Nth page (0-based).
    /// Returns default US Letter [0, 0, 612, 792] if not found.
    pub fn get_page_media_box(data: &[u8], page_num: i32) -> [f32; 4] {
        const DEFAULT: [f32; 4] = [0.0, 0.0, 612.0, 792.0];
        if page_num < 0 {
            return DEFAULT;
        }
        let page_num = page_num as usize;

        // Get root -> catalog -> /Pages
        let root_ref = match Self::get_root_ref_from_trailer(data) {
            Some(r) => r,
            None => return DEFAULT,
        };
        let catalog_data = match Self::find_object_content(data, root_ref.0, root_ref.1) {
            Some(d) => d,
            None => return DEFAULT,
        };
        let pages_ref = match Self::extract_ref_from_dict(catalog_data, b"/Pages") {
            Some(r) => r,
            None => return DEFAULT,
        };

        // Collect page object refs by traversing /Kids
        let mut page_refs = Vec::new();
        if !Self::collect_page_refs(data, pages_ref.0, pages_ref.1, &mut page_refs) {
            return DEFAULT;
        }

        let (obj_num, gen_num) = match page_refs.get(page_num) {
            Some(r) => *r,
            None => return DEFAULT,
        };

        let page_data = match Self::find_object_content(data, obj_num, gen_num) {
            Some(d) => d,
            None => return DEFAULT,
        };

        Self::extract_mediabox_from_dict(page_data).unwrap_or(DEFAULT)
    }

    /// Get /Root ref from trailer (shared logic for page count and media box)
    fn get_root_ref_from_trailer(data: &[u8]) -> Option<(i32, i32)> {
        let search_start = data.len().saturating_sub(2048);
        let search_region = &data[search_start..];
        let startxref_pos = Self::find_last_pattern(search_region, b"startxref")?;
        let abs_startxref = search_start + startxref_pos;
        let after_startxref = &data[abs_startxref + 9..];
        let xref_offset = Self::parse_trailing_number(after_startxref)?;

        if xref_offset < data.len() {
            let at_offset = &data[xref_offset..];
            if at_offset.starts_with(b"xref") {
                Self::get_root_from_trailer(data, abs_startxref)
            } else {
                Self::get_root_from_xref_stream(data, xref_offset)
            }
        } else {
            Self::get_root_from_trailer(data, abs_startxref)
        }
    }

    /// Recursively collect page object refs from a Pages node
    fn collect_page_refs(
        data: &[u8],
        obj_num: i32,
        gen_num: i32,
        out: &mut Vec<(i32, i32)>,
    ) -> bool {
        let pages_data = match Self::find_object_content(data, obj_num, gen_num) {
            Some(d) => d,
            None => return false,
        };

        let kids_refs = Self::extract_kids_array(pages_data);
        for (kid_num, kid_gen) in kids_refs {
            if Self::is_page_object(data, kid_num, kid_gen) {
                out.push((kid_num, kid_gen));
            } else if Self::is_pages_object(data, kid_num, kid_gen) {
                if !Self::collect_page_refs(data, kid_num, kid_gen, out) {
                    return false;
                }
            }
        }
        true
    }

    fn is_page_object(data: &[u8], obj_num: i32, gen_num: i32) -> bool {
        if let Some(content) = Self::find_object_content(data, obj_num, gen_num) {
            (Self::find_pattern(content, b"/Type /Page").is_some()
                || Self::find_pattern(content, b"/Type/Page").is_some())
                && Self::find_pattern(content, b"/Type /Pages").is_none()
                && Self::find_pattern(content, b"/Type/Pages").is_none()
        } else {
            false
        }
    }

    fn is_pages_object(data: &[u8], obj_num: i32, gen_num: i32) -> bool {
        if let Some(content) = Self::find_object_content(data, obj_num, gen_num) {
            Self::find_pattern(content, b"/Type /Pages").is_some()
                || Self::find_pattern(content, b"/Type/Pages").is_some()
        } else {
            false
        }
    }

    /// Extract /Kids array as list of (obj_num, gen_num) refs
    fn extract_kids_array(data: &[u8]) -> Vec<(i32, i32)> {
        let mut refs = Vec::new();
        let kids_pos = match Self::find_pattern(data, b"/Kids")
            .or_else(|| Self::find_pattern(data, b"/Kids "))
        {
            Some(p) => p,
            None => return refs,
        };
        let after = &data[kids_pos + 5..];
        let bracket = match after.iter().position(|&b| b == b'[') {
            Some(p) => p,
            None => return refs,
        };
        let array_start = kids_pos + 5 + bracket + 1;
        let array_content = &data[array_start..];
        let mut i = 0;
        while i < array_content.len() {
            if array_content[i] == b']' {
                break;
            }
            if let Some((obj_num, gen_num, consumed)) = Self::parse_next_ref(&array_content[i..]) {
                refs.push((obj_num, gen_num));
                i += consumed;
            } else {
                i += 1;
            }
            while i < array_content.len()
                && (array_content[i] == b' '
                    || array_content[i] == b'\t'
                    || array_content[i] == b'\n'
                    || array_content[i] == b'\r')
            {
                i += 1;
            }
        }
        refs
    }

    /// Parse "N M R" or "N M R " from start of slice, return (obj_num, gen_num, bytes_consumed)
    fn parse_next_ref(data: &[u8]) -> Option<(i32, i32, usize)> {
        let mut i = 0;
        while i < data.len()
            && (data[i] == b' ' || data[i] == b'\t' || data[i] == b'\n' || data[i] == b'\r')
        {
            i += 1;
        }
        let num_start = i;
        while i < data.len() && (data[i].is_ascii_digit() || (i == num_start && data[i] == b'-')) {
            i += 1;
        }
        let obj_num: i32 = std::str::from_utf8(&data[num_start..i])
            .ok()?
            .parse()
            .ok()?;
        while i < data.len()
            && (data[i] == b' ' || data[i] == b'\t' || data[i] == b'\n' || data[i] == b'\r')
        {
            i += 1;
        }
        let gen_start = i;
        while i < data.len() && data[i].is_ascii_digit() {
            i += 1;
        }
        let gen_num: i32 = std::str::from_utf8(&data[gen_start..i])
            .ok()?
            .parse()
            .ok()?;
        while i < data.len()
            && (data[i] == b' ' || data[i] == b'\t' || data[i] == b'\n' || data[i] == b'\r')
        {
            i += 1;
        }
        if i < data.len() && data[i] == b'R' {
            Some((obj_num, gen_num, i + 1))
        } else {
            None
        }
    }

    /// Extract /MediaBox [x0 y0 x1 y1] from page dict
    fn extract_mediabox_from_dict(data: &[u8]) -> Option<[f32; 4]> {
        let mediabox_pos = Self::find_pattern(data, b"/MediaBox")
            .or_else(|| Self::find_pattern(data, b"/MediaBox "))?;
        let after = &data[mediabox_pos + 9..];
        let bracket = after.iter().position(|&b| b == b'[')?;
        let array_start = mediabox_pos + 9 + bracket + 1;
        let array_content = &data[array_start..];
        let mut nums = [0.0f32; 4];
        let mut i = 0;
        for num in &mut nums {
            while i < array_content.len()
                && (array_content[i] == b' '
                    || array_content[i] == b'\t'
                    || array_content[i] == b'\n'
                    || array_content[i] == b'\r')
            {
                i += 1;
            }
            if i >= array_content.len() || array_content[i] == b']' {
                return None;
            }
            let start = i;
            while i < array_content.len()
                && (array_content[i].is_ascii_digit()
                    || array_content[i] == b'.'
                    || array_content[i] == b'-')
            {
                i += 1;
            }
            *num = std::str::from_utf8(&array_content[start..i])
                .ok()?
                .parse()
                .ok()?;
        }
        Some(nums)
    }
}

/// Open a document from file
///
/// # Safety
/// Caller must ensure `filename` is a valid null-terminated C string.
#[unsafe(no_mangle)]
pub extern "C" fn fz_open_document(_ctx: Handle, filename: *const c_char) -> Handle {
    match catch_unwind(AssertUnwindSafe(|| {
        if filename.is_null() {
            return 0;
        }

        // SAFETY: Caller guarantees filename is a valid null-terminated C string
        let c_str = unsafe { std::ffi::CStr::from_ptr(filename) };
        let path = match c_str.to_str() {
            Ok(s) => s,
            Err(_) => return 0,
        };

        match std::fs::read(path) {
            Ok(data) => {
                if data.is_empty() {
                    eprintln!("fz_open_document: file is empty: {}", path);
                    return 0;
                }
                if data.len() < 5 || &data[0..5] != b"%PDF-" {
                    eprintln!(
                        "fz_open_document: not a valid PDF (bad magic bytes): {}",
                        path
                    );
                    return 0;
                }
                DOCUMENTS.insert(Document::new(data))
            }
            Err(e) => {
                eprintln!("fz_open_document: failed to read '{}': {}", path, e);
                0
            }
        }
    })) {
        Ok(handle) => handle,
        Err(panic_info) => {
            let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
                format!("fz_open_document: panic: {}", s)
            } else if let Some(s) = panic_info.downcast_ref::<String>() {
                format!("fz_open_document: panic: {}", s)
            } else {
                "fz_open_document: panic (unknown payload)".to_string()
            };
            eprintln!("{}", msg);
            0
        }
    }
}

/// Open a document from stream
#[unsafe(no_mangle)]
pub extern "C" fn fz_open_document_with_stream(
    _ctx: Handle,
    _magic: *const c_char,
    stm: Handle,
) -> Handle {
    match catch_unwind(AssertUnwindSafe(|| {
        // Read all data from stream
        if let Some(stream) = STREAMS.get(stm) {
            if let Ok(guard) = stream.lock() {
                return DOCUMENTS.insert(Document::new(guard.data.clone()));
            }
        }
        0
    })) {
        Ok(handle) => handle,
        Err(panic_info) => {
            let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
                format!("fz_open_document_with_stream: panic: {}", s)
            } else if let Some(s) = panic_info.downcast_ref::<String>() {
                format!("fz_open_document_with_stream: panic: {}", s)
            } else {
                "fz_open_document_with_stream: panic (unknown payload)".to_string()
            };
            eprintln!("{}", msg);
            0
        }
    }
}

/// Keep (increment ref) document
#[unsafe(no_mangle)]
pub extern "C" fn fz_keep_document(_ctx: Handle, doc: Handle) -> Handle {
    DOCUMENTS.keep(doc)
}

/// Drop document reference
#[unsafe(no_mangle)]
pub extern "C" fn fz_drop_document(_ctx: Handle, doc: Handle) {
    let _ = DOCUMENTS.remove(doc);
}

/// Check if document needs a password
#[unsafe(no_mangle)]
pub extern "C" fn fz_needs_password(_ctx: Handle, doc: Handle) -> i32 {
    if let Some(d) = DOCUMENTS.get(doc) {
        if let Ok(guard) = d.lock() {
            return i32::from(guard.needs_password);
        }
    }
    0
}

/// Authenticate with password
#[unsafe(no_mangle)]
pub extern "C" fn fz_authenticate_password(
    _ctx: Handle,
    doc: Handle,
    password: *const c_char,
) -> i32 {
    if password.is_null() {
        return 0;
    }

    let password_str = unsafe {
        match std::ffi::CStr::from_ptr(password).to_str() {
            Ok(s) => s,
            Err(_) => return 0,
        }
    };

    if let Some(document) = DOCUMENTS.get(doc) {
        if let Ok(mut d) = document.lock() {
            // If no password needed, succeed
            if !d.needs_password {
                d.authenticated = true;
                return 1;
            }

            // Verify password matches
            if let Some(ref stored_password) = d.password {
                if stored_password == password_str {
                    d.authenticated = true;
                    return 1;
                }
            }
        }
    }
    0
}

/// Count pages in document
#[unsafe(no_mangle)]
pub extern "C" fn fz_count_pages(_ctx: Handle, doc: Handle) -> i32 {
    if let Some(d) = DOCUMENTS.get(doc) {
        if let Ok(guard) = d.lock() {
            return guard.page_count;
        }
    }
    0
}

/// Count chapters in document (PDF has 1 chapter)
#[unsafe(no_mangle)]
pub extern "C" fn fz_count_chapters(_ctx: Handle, _doc: Handle) -> i32 {
    1
}

/// Count pages in chapter
#[unsafe(no_mangle)]
pub extern "C" fn fz_count_chapter_pages(_ctx: Handle, doc: Handle, _chapter: i32) -> i32 {
    fz_count_pages(_ctx, doc)
}

/// Get page number from location
#[unsafe(no_mangle)]
pub extern "C" fn fz_page_number_from_location(
    _ctx: Handle,
    _doc: Handle,
    chapter: i32,
    page: i32,
) -> i32 {
    if chapter == 0 { page } else { -1 }
}

/// Check document permission by reading the /Encrypt dictionary's /P
/// entry.  Unencrypted documents grant all permissions.
#[unsafe(no_mangle)]
pub extern "C" fn fz_has_permission(_ctx: Handle, doc: Handle, permission: i32) -> i32 {
    if let Some(d) = DOCUMENTS.get(doc) {
        if let Ok(guard) = d.lock() {
            let data = guard.data();
            let perms = match get_permissions_value(data) {
                Some(p) => p,
                None => return 1, // No encryption -> all allowed
            };
            // Map MuPDF constants to PDF spec permission bits
            let pdf_bit = match permission {
                FZ_PERMISSION_PRINT => 1 << 2,    // bit 3 (value 4)
                FZ_PERMISSION_COPY => 1 << 4,     // bit 5 (value 16)
                FZ_PERMISSION_EDIT => 1 << 3,     // bit 4 (value 8)
                FZ_PERMISSION_ANNOTATE => 1 << 5, // bit 6 (value 32)
                _ => return 0,
            };
            return if (perms & pdf_bit) != 0 { 1 } else { 0 };
        }
    }
    0
}

// Permission flags
pub const FZ_PERMISSION_PRINT: i32 = 1 << 0;
pub const FZ_PERMISSION_COPY: i32 = 1 << 1;
pub const FZ_PERMISSION_EDIT: i32 = 1 << 2;
pub const FZ_PERMISSION_ANNOTATE: i32 = 1 << 3;

/// Lookup metadata from the PDF /Info dictionary.
///
/// Accepts MuPDF-style keys ("info:Title", "info:Author", etc.) and
/// also bare names ("Title", "Author").  Returns the byte length of the
/// value written, or -1 when the key is not found.
///
/// # Safety
/// Caller must ensure `buf` points to writable memory of at least `size` bytes.
#[unsafe(no_mangle)]
pub extern "C" fn fz_lookup_metadata(
    _ctx: Handle,
    doc: Handle,
    key: *const c_char,
    buf: *mut c_char,
    size: i32,
) -> i32 {
    if key.is_null() {
        if !buf.is_null() && size > 0 {
            unsafe {
                *buf = 0;
            }
        }
        return -1;
    }
    let key_str = match unsafe { std::ffi::CStr::from_ptr(key) }.to_str() {
        Ok(s) => s,
        Err(_) => {
            if !buf.is_null() && size > 0 {
                unsafe {
                    *buf = 0;
                }
            }
            return -1;
        }
    };

    if let Some(d) = DOCUMENTS.get(doc) {
        if let Ok(guard) = d.lock() {
            if key_str == "format" {
                return write_metadata_value(&guard.format, buf, size);
            }
            let data = guard.data();
            let info_key = key_str.strip_prefix("info:").unwrap_or(key_str);
            let pdf_key = match info_key {
                "Title" | "Author" | "Subject" | "Keywords" | "Creator" | "Producer"
                | "CreationDate" | "ModDate" => info_key,
                _ => {
                    if !buf.is_null() && size > 0 {
                        unsafe {
                            *buf = 0;
                        }
                    }
                    return -1;
                }
            };
            if let Some(value) = lookup_info_metadata(data, pdf_key) {
                return write_metadata_value(&value, buf, size);
            }
        }
    }
    if !buf.is_null() && size > 0 {
        unsafe {
            *buf = 0;
        }
    }
    -1
}

// ============================================================================
// Page Functions
// ============================================================================

/// Load a page from document
#[unsafe(no_mangle)]
pub extern "C" fn fz_load_page(_ctx: Handle, doc: Handle, page_num: i32) -> Handle {
    match catch_unwind(AssertUnwindSafe(|| {
        if let Some(document) = DOCUMENTS.get(doc) {
            if let Ok(guard) = document.lock() {
                let page_count = guard.page_count;
                if page_num >= 0 && page_num < page_count {
                    let bounds = Document::get_page_media_box(guard.data(), page_num);
                    return PAGES.insert(Page::with_bounds(doc, page_num, bounds));
                }
            }
        }
        0
    })) {
        Ok(handle) => handle,
        Err(panic_info) => {
            let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
                format!("fz_load_page: panic: {}", s)
            } else if let Some(s) = panic_info.downcast_ref::<String>() {
                format!("fz_load_page: panic: {}", s)
            } else {
                "fz_load_page: panic (unknown payload)".to_string()
            };
            eprintln!("{}", msg);
            0
        }
    }
}

/// Load page by location (chapter, page)
#[unsafe(no_mangle)]
pub extern "C" fn fz_load_chapter_page(
    _ctx: Handle,
    doc: Handle,
    chapter: i32,
    page: i32,
) -> Handle {
    if chapter != 0 {
        return 0; // PDF only has chapter 0
    }
    fz_load_page(_ctx, doc, page)
}

/// Keep (increment ref) page
#[unsafe(no_mangle)]
pub extern "C" fn fz_keep_page(_ctx: Handle, page: Handle) -> Handle {
    PAGES.keep(page)
}

/// Drop page reference
#[unsafe(no_mangle)]
pub extern "C" fn fz_drop_page(_ctx: Handle, page: Handle) {
    let _ = PAGES.remove(page);
}

/// Get page bounds
#[unsafe(no_mangle)]
pub extern "C" fn fz_bound_page(_ctx: Handle, page: Handle) -> super::geometry::fz_rect {
    if let Some(p) = PAGES.get(page) {
        if let Ok(guard) = p.lock() {
            return super::geometry::fz_rect {
                x0: guard.bounds[0],
                y0: guard.bounds[1],
                x1: guard.bounds[2],
                y1: guard.bounds[3],
            };
        }
    }
    super::geometry::fz_rect {
        x0: 0.0,
        y0: 0.0,
        x1: 0.0,
        y1: 0.0,
    }
}

/// Get page rotation (0, 90, 180, or 270).
///
/// Reads the `/Rotate` entry from the page dictionary in the raw PDF,
/// falling back to 0 when absent.
#[unsafe(no_mangle)]
pub extern "C" fn fz_page_rotation(_ctx: Handle, page: Handle) -> i32 {
    if let Some(p) = PAGES.get(page) {
        if let Ok(guard) = p.lock() {
            if let Some(doc_arc) = DOCUMENTS.get(guard.doc_handle) {
                if let Ok(dg) = doc_arc.lock() {
                    let data = dg.data();
                    if let Some((ds, de)) = find_page_dict(data, guard.page_num) {
                        return parse_rotation_from_dict(data, ds, de);
                    }
                }
            }
        }
    }
    0
}

/// Parse the /Rotate value from a page dictionary byte range.
fn parse_rotation_from_dict(data: &[u8], dict_start: usize, dict_end: usize) -> i32 {
    let region = &data[dict_start..dict_end.min(data.len())];
    let pattern = b"/Rotate";
    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 negative = pos < after.len() && after[pos] == b'-';
            if negative {
                pos += 1;
            }
            let mut end = pos;
            while end < after.len() && after[end].is_ascii_digit() {
                end += 1;
            }
            if end > pos {
                if let Ok(val) = std::str::from_utf8(&after[pos..end])
                    .unwrap_or("0")
                    .parse::<i32>()
                {
                    let rotation = if negative { -val } else { val };
                    return ((rotation % 360) + 360) % 360;
                }
            }
        }
    }
    0
}

/// Get page bounds with specified box type (0=Media, 1=Crop, 2=Bleed,
/// 3=Trim, 4=Art).  Falls back to MediaBox when the requested box is
/// absent.
#[unsafe(no_mangle)]
pub extern "C" fn fz_bound_page_box(
    _ctx: Handle,
    page: Handle,
    box_type: i32,
) -> super::geometry::fz_rect {
    let box_name: &[u8] = match box_type {
        1 => b"/CropBox",
        2 => b"/BleedBox",
        3 => b"/TrimBox",
        4 => b"/ArtBox",
        _ => b"/MediaBox",
    };
    if let Some(p) = PAGES.get(page) {
        if let Ok(guard) = p.lock() {
            if let Some(doc_arc) = DOCUMENTS.get(guard.doc_handle) {
                if let Ok(dg) = doc_arc.lock() {
                    let data = dg.data();
                    if let Some((ds, de)) = find_page_dict(data, guard.page_num) {
                        if let Some(r) = parse_page_box(data, ds, de, box_name) {
                            return super::geometry::fz_rect {
                                x0: r[0],
                                y0: r[1],
                                x1: r[2],
                                y1: r[3],
                            };
                        }
                    }
                }
            }
            return super::geometry::fz_rect {
                x0: guard.bounds[0],
                y0: guard.bounds[1],
                x1: guard.bounds[2],
                y1: guard.bounds[3],
            };
        }
    }
    super::geometry::fz_rect {
        x0: 0.0,
        y0: 0.0,
        x1: 0.0,
        y1: 0.0,
    }
}

/// Render page contents and annotations to device.
///
/// # Safety
/// Caller must ensure device is valid
#[unsafe(no_mangle)]
pub extern "C" fn fz_run_page(
    _ctx: Handle,
    page: Handle,
    device: Handle,
    transform: super::geometry::fz_matrix,
    cookie: *mut std::ffi::c_void,
) {
    fz_run_page_contents(_ctx, page, device, transform, cookie);
    fz_run_page_annots(_ctx, page, device, transform, cookie);
}

/// Render page contents to device (excludes annotations).
///
/// Extracts the /Contents stream from the page dictionary and
/// interprets it using `crate::pdf::interpret::Interpreter`.
///
/// # Safety
/// Caller must ensure device is valid
#[unsafe(no_mangle)]
pub extern "C" fn fz_run_page_contents(
    _ctx: Handle,
    page: Handle,
    device: Handle,
    transform: super::geometry::fz_matrix,
    cookie: *mut std::ffi::c_void,
) {
    if !cookie.is_null() {
        let cookie_handle = cookie as Handle;
        if let Some(c) = super::cookie::COOKIES.get(cookie_handle) {
            if let Ok(guard) = c.lock() {
                if guard.should_abort() {
                    return;
                }
            }
        }
    }

    let (doc_handle, page_num) = if let Some(p) = PAGES.get(page) {
        if let Ok(guard) = p.lock() {
            (guard.doc_handle, guard.page_num)
        } else {
            return;
        }
    } else {
        return;
    };

    let dev_arc = match super::device::DEVICES.get(device) {
        Some(d) => d,
        None => return,
    };

    let content_stream = if let Some(doc_arc) = DOCUMENTS.get(doc_handle) {
        if let Ok(dg) = doc_arc.lock() {
            let data = dg.data();
            find_page_dict(data, page_num).and_then(|(ds, de)| extract_page_contents(data, ds, de))
        } else {
            None
        }
    } else {
        return;
    };

    if let Some(stream_data) = content_stream {
        let _matrix = crate::fitz::geometry::Matrix {
            a: transform.a,
            b: transform.b,
            c: transform.c,
            d: transform.d,
            e: transform.e,
            f: transform.f,
        };
        let mut interpreter = crate::pdf::interpret::Interpreter::new();
        if let Ok(mut dg) = dev_arc.lock() {
            let _ = interpreter.interpret(&stream_data, &mut *dg);
        }
    }
}

/// Render page annotations to device
///
/// # Safety
/// Caller must ensure device is valid
#[unsafe(no_mangle)]
pub extern "C" fn fz_run_page_annots(
    _ctx: Handle,
    page: Handle,
    device: Handle,
    transform: super::geometry::fz_matrix,
    cookie: *mut std::ffi::c_void,
) {
    // Check for cancellation via cookie
    if !cookie.is_null() {
        let cookie_handle = cookie as Handle;
        if let Some(c) = super::cookie::COOKIES.get(cookie_handle) {
            if let Ok(guard) = c.lock() {
                if guard.should_abort() {
                    return; // Operation cancelled
                }
            }
        }
    }

    // Get annotations for this page
    let annot_handles = if let Some(p) = PAGES.get(page) {
        if let Ok(guard) = p.lock() {
            guard.annotations.clone()
        } else {
            return;
        }
    } else {
        return;
    };

    // Verify device exists
    if super::device::DEVICES.get(device).is_none() {
        return;
    }

    // Convert transform to Matrix
    let _matrix = crate::fitz::geometry::Matrix {
        a: transform.a,
        b: transform.b,
        c: transform.c,
        d: transform.d,
        e: transform.e,
        f: transform.f,
    };

    // Render each annotation
    for annot_handle in annot_handles {
        if let Some(annot_arc) = super::annot::ANNOTATIONS.get(annot_handle) {
            if let Ok(_annot_guard) = annot_arc.lock() {
                // For each annotation, we would:
                // 1. Get annotation rectangle and transform it
                // 2. Render annotation appearance (AP stream) if available
                // 3. Fall back to rendering based on annotation type
                //
                // Since annotation rendering requires appearance stream parsing,
                // this establishes the API structure for when that's implemented
            }
        }
    }
}

// ============================================================================
// Outline Functions
// ============================================================================

/// Load document outline (table of contents).
///
/// Parses the /Outlines tree from the document catalog.  Returns an
/// empty outline node when the document has no TOC (the returned
/// handle is still valid so callers can inspect it).
#[unsafe(no_mangle)]
pub extern "C" fn fz_load_outline(_ctx: Handle, doc: Handle) -> Handle {
    if let Some(d) = DOCUMENTS.get(doc) {
        if let Ok(guard) = d.lock() {
            if let Some(handle) = build_outline_from_pdf(guard.data()) {
                return handle;
            }
        }
    } else {
        return 0; // Invalid doc handle
    }
    // No outlines found -- return an empty outline node
    OUTLINES.insert(super::outline::Outline::default())
}

// Note: fz_drop_outline is defined in outline.rs

// ============================================================================
// Link Resolution
// ============================================================================

/// Resolve a link destination to a location
#[unsafe(no_mangle)]
pub extern "C" fn fz_resolve_link(
    _ctx: Handle,
    doc: Handle,
    uri: *const c_char,
    xp: *mut f32,
    yp: *mut f32,
) -> i32 {
    if uri.is_null() || DOCUMENTS.get(doc).is_none() {
        return -1;
    }

    // SAFETY: Caller guarantees uri is a valid null-terminated C string
    let c_str = unsafe { std::ffi::CStr::from_ptr(uri) };
    let uri_str = match c_str.to_str() {
        Ok(s) => s,
        Err(_) => return -1,
    };

    // Parse page number from URI (e.g., "#page=5" or just "5")
    let page_num = if let Some(num_str) = uri_str.strip_prefix("#page=") {
        num_str.parse::<i32>().ok()
    } else if let Some(num_str) = uri_str.strip_prefix('#') {
        num_str.parse::<i32>().ok()
    } else {
        uri_str.parse::<i32>().ok()
    };

    match page_num {
        Some(n) => {
            // Set coordinates to top-left of page
            if !xp.is_null() {
                unsafe {
                    *xp = 0.0;
                }
            }
            if !yp.is_null() {
                unsafe {
                    *yp = 0.0;
                }
            }
            n
        }
        None => -1,
    }
}

/// Make a URI from a page location
///
/// # Safety
/// Caller must ensure `buf` points to writable memory of at least `size` bytes.
#[unsafe(no_mangle)]
pub extern "C" fn fz_make_location_uri(
    _ctx: Handle,
    _doc: Handle,
    page: i32,
    buf: *mut c_char,
    size: i32,
) -> *mut c_char {
    if buf.is_null() || size <= 0 {
        return std::ptr::null_mut();
    }

    let uri = format!("#page={}", page);
    let uri_bytes = uri.as_bytes();
    let copy_len = (uri_bytes.len()).min((size - 1) as usize);

    unsafe {
        std::ptr::copy_nonoverlapping(uri_bytes.as_ptr(), buf as *mut u8, copy_len);
        *buf.add(copy_len) = 0; // Null terminate
    }

    buf
}

/// Get document format name
#[unsafe(no_mangle)]
pub extern "C" fn fz_document_format(
    _ctx: Handle,
    doc: Handle,
    buf: *mut c_char,
    size: i32,
) -> i32 {
    if buf.is_null() || size <= 0 {
        return 0;
    }

    if let Some(d) = DOCUMENTS.get(doc) {
        if let Ok(guard) = d.lock() {
            let format = &guard.format;
            let bytes = format.as_bytes();
            let copy_len = (bytes.len()).min((size - 1) as usize);

            unsafe {
                std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
                *buf.add(copy_len) = 0;
            }
            return copy_len as i32;
        }
    }
    0
}

/// Check if document is reflowable
#[unsafe(no_mangle)]
pub extern "C" fn fz_is_document_reflowable(_ctx: Handle, doc: Handle) -> i32 {
    // PDF documents are fixed-layout, not reflowable
    // EPUB and other formats would be reflowable
    if let Some(d) = DOCUMENTS.get(doc) {
        if let Ok(guard) = d.lock() {
            // Check format - only EPUB and similar formats are reflowable
            return if guard.format.to_lowercase().contains("epub") {
                1
            } else {
                0
            };
        }
    }
    0
}

/// Layout document for given dimensions (for reflowable documents only)
#[unsafe(no_mangle)]
pub extern "C" fn fz_layout_document(
    _ctx: Handle,
    doc: Handle,
    _w: c_float,
    _h: c_float,
    _em: c_float,
) {
    // This function is only relevant for reflowable documents (EPUB, etc.)
    // PDF documents are fixed-layout and ignore layout calls
    if let Some(d) = DOCUMENTS.get(doc) {
        if let Ok(_guard) = d.lock() {
            // For reflowable formats, this would:
            // 1. Reflow text to fit width 'w' and height 'h'
            // 2. Use 'em' as the base font size
            // 3. Recalculate page breaks
            //
            // Since PDF is fixed-layout, this is a no-op
            // but we maintain the API for compatibility
        }
    }
}

/// Get page label from the /PageLabels number tree.
///
/// Falls back to the 1-based page number string when no /PageLabels
/// entry exists in the document catalog.
#[unsafe(no_mangle)]
pub extern "C" fn fz_page_label(
    _ctx: Handle,
    doc: Handle,
    page_num: i32,
    buf: *mut c_char,
    size: i32,
) -> i32 {
    if buf.is_null() || size <= 0 {
        return 0;
    }

    if let Some(d) = DOCUMENTS.get(doc) {
        if let Ok(guard) = d.lock() {
            if page_num >= 0 && page_num < guard.page_count {
                let label = compute_page_label(guard.data(), page_num)
                    .unwrap_or_else(|| format!("{}", page_num + 1));
                let bytes = label.as_bytes();
                let copy_len = bytes.len().min((size - 1) as usize);

                unsafe {
                    std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
                    *buf.add(copy_len) = 0;
                }
                return copy_len as i32;
            }
        }
    }
    0
}

/// Check if document is valid
#[unsafe(no_mangle)]
pub extern "C" fn fz_document_is_valid(_ctx: Handle, doc: Handle) -> i32 {
    if DOCUMENTS.get(doc).is_some() { 1 } else { 0 }
}

/// Clone a document (increase ref count)
#[unsafe(no_mangle)]
pub extern "C" fn fz_clone_document(_ctx: Handle, doc: Handle) -> Handle {
    fz_keep_document(_ctx, doc)
}

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

    #[test]
    fn test_document_handle() {
        // Create a minimal "PDF" for testing
        let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());

        let handle = DOCUMENTS.insert(doc);
        assert_ne!(handle, 0);

        assert_eq!(fz_count_chapters(0, handle), 1);
        assert!(fz_count_pages(0, handle) >= 1);

        fz_drop_document(0, handle);
    }

    #[test]
    fn test_document_new() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        assert_eq!(doc.page_count, 2);
        assert!(!doc.needs_password);
        assert!(doc.authenticated);
    }

    #[test]
    fn test_document_estimate_page_count() {
        // No pages - fallback to 1
        let empty = b"%PDF-1.4\n%%EOF";
        let doc1 = Document::new(empty.to_vec());
        assert_eq!(doc1.page_count, 1); // Minimum 1

        // Multiple pages via heuristics
        let multi = b"%PDF-1.4\n/Type /Page\n/Type /Page\n/Type /Page\n%%EOF";
        let doc2 = Document::new(multi.to_vec());
        assert_eq!(doc2.page_count, 3);

        // Proper trailer parsing - minimal valid PDF with correct structure
        let proper_pdf = b"%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
endobj
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] >>
endobj
xref
0 5
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000127 00000 n
0000000200 00000 n
trailer << /Size 5 /Root 1 0 R >>
startxref
273
%%EOF";
        let doc3 = Document::new(proper_pdf.to_vec());
        assert_eq!(doc3.page_count, 2, "proper_page_count should parse trailer");
    }

    #[test]
    fn test_get_page_media_box() {
        // Minimal PDF with proper structure
        let pdf_data = b"%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer << /Size 4 /Root 1 0 R >>
startxref
188
%%EOF";
        let bounds = Document::get_page_media_box(pdf_data, 0);
        assert_eq!(bounds, [0.0, 0.0, 595.0, 842.0], "A4 MediaBox");
    }

    #[test]
    fn test_page_with_bounds() {
        let page = Page::with_bounds(1, 0, [10.0, 20.0, 595.0, 842.0]);
        assert_eq!(page.bounds[0], 10.0);
        assert_eq!(page.bounds[1], 20.0);
        assert_eq!(page.bounds[2], 595.0);
        assert_eq!(page.bounds[3], 842.0);
    }

    #[test]
    fn test_keep_document() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let handle = DOCUMENTS.insert(doc);

        let kept = fz_keep_document(0, handle);
        assert_eq!(kept, handle);

        fz_drop_document(0, handle);
    }

    #[test]
    fn test_needs_password() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let handle = DOCUMENTS.insert(doc);

        assert_eq!(fz_needs_password(0, handle), 0);

        fz_drop_document(0, handle);
    }

    #[test]
    fn test_needs_password_invalid_handle() {
        assert_eq!(fz_needs_password(0, 0), 0);
    }

    #[test]
    fn test_authenticate_password() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let handle = DOCUMENTS.insert(doc);

        // No password needed, should succeed
        let result = fz_authenticate_password(0, handle, c"".as_ptr());
        assert_eq!(result, 1);

        fz_drop_document(0, handle);
    }

    #[test]
    fn test_authenticate_password_invalid_handle() {
        let result = fz_authenticate_password(0, 0, c"".as_ptr());
        assert_eq!(result, 0);
    }

    #[test]
    fn test_count_pages() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let handle = DOCUMENTS.insert(doc);

        assert_eq!(fz_count_pages(0, handle), 2);

        fz_drop_document(0, handle);
    }

    #[test]
    fn test_count_pages_invalid_handle() {
        assert_eq!(fz_count_pages(0, 0), 0);
    }

    #[test]
    fn test_count_chapters() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let handle = DOCUMENTS.insert(doc);

        // PDFs always have 1 chapter
        assert_eq!(fz_count_chapters(0, handle), 1);

        fz_drop_document(0, handle);
    }

    #[test]
    fn test_count_chapter_pages() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let handle = DOCUMENTS.insert(doc);

        assert_eq!(fz_count_chapter_pages(0, handle, 0), 2);

        fz_drop_document(0, handle);
    }

    #[test]
    fn test_page_number_from_location() {
        assert_eq!(fz_page_number_from_location(0, 0, 0, 5), 5);
        assert_eq!(fz_page_number_from_location(0, 0, 0, 0), 0);
        assert_eq!(fz_page_number_from_location(0, 0, 1, 5), -1); // Invalid chapter
    }

    #[test]
    fn test_has_permission() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let handle = DOCUMENTS.insert(doc);

        assert_eq!(fz_has_permission(0, handle, FZ_PERMISSION_PRINT), 1);
        assert_eq!(fz_has_permission(0, handle, FZ_PERMISSION_COPY), 1);
        assert_eq!(fz_has_permission(0, handle, FZ_PERMISSION_EDIT), 1);
        assert_eq!(fz_has_permission(0, handle, FZ_PERMISSION_ANNOTATE), 1);

        fz_drop_document(0, handle);
    }

    #[test]
    fn test_has_permission_invalid_handle() {
        assert_eq!(fz_has_permission(0, 0, FZ_PERMISSION_PRINT), 0);
    }

    #[test]
    fn test_lookup_metadata() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let handle = DOCUMENTS.insert(doc);

        let mut buf = [0i8; 100];
        let result = fz_lookup_metadata(0, handle, c"Title".as_ptr(), buf.as_mut_ptr(), 100);
        assert_eq!(result, -1); // Not found

        fz_drop_document(0, handle);
    }

    #[test]
    fn test_lookup_metadata_null_buffer() {
        let result = fz_lookup_metadata(0, 0, c"Title".as_ptr(), std::ptr::null_mut(), 0);
        assert_eq!(result, -1);
    }

    #[test]
    fn test_open_document_null_filename() {
        let handle = fz_open_document(0, std::ptr::null());
        assert_eq!(handle, 0);
    }

    #[test]
    fn test_open_document_with_stream() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
        let stream = Stream::from_memory(pdf_data.to_vec());
        let stream_handle = STREAMS.insert(stream);

        let doc_handle = fz_open_document_with_stream(0, std::ptr::null(), stream_handle);
        assert_ne!(doc_handle, 0);

        assert_eq!(fz_count_pages(0, doc_handle), 1);

        fz_drop_document(0, doc_handle);
        super::super::STREAMS.remove(stream_handle);
    }

    #[test]
    fn test_open_document_with_invalid_stream() {
        let doc_handle = fz_open_document_with_stream(0, std::ptr::null(), 0);
        assert_eq!(doc_handle, 0);
    }

    #[test]
    fn test_permission_constants() {
        assert_eq!(FZ_PERMISSION_PRINT, 1);
        assert_eq!(FZ_PERMISSION_COPY, 2);
        assert_eq!(FZ_PERMISSION_EDIT, 4);
        assert_eq!(FZ_PERMISSION_ANNOTATE, 8);
    }

    // ============================================================================
    // Page Tests
    // ============================================================================

    #[test]
    fn test_load_page() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let doc_handle = DOCUMENTS.insert(doc);

        let page_handle = fz_load_page(0, doc_handle, 0);
        assert_ne!(page_handle, 0);

        fz_drop_page(0, page_handle);
        fz_drop_document(0, doc_handle);
    }

    #[test]
    fn test_load_page_invalid_doc() {
        let page_handle = fz_load_page(0, 0, 0);
        assert_eq!(page_handle, 0);
    }

    #[test]
    fn test_load_page_invalid_page_num() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let doc_handle = DOCUMENTS.insert(doc);

        // Negative page
        let page1 = fz_load_page(0, doc_handle, -1);
        assert_eq!(page1, 0);

        // Out of bounds
        let page2 = fz_load_page(0, doc_handle, 100);
        assert_eq!(page2, 0);

        fz_drop_document(0, doc_handle);
    }

    #[test]
    fn test_load_chapter_page() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let doc_handle = DOCUMENTS.insert(doc);

        // Chapter 0 should work
        let page1 = fz_load_chapter_page(0, doc_handle, 0, 0);
        assert_ne!(page1, 0);

        // Chapter 1 should fail
        let page2 = fz_load_chapter_page(0, doc_handle, 1, 0);
        assert_eq!(page2, 0);

        fz_drop_page(0, page1);
        fz_drop_document(0, doc_handle);
    }

    #[test]
    fn test_keep_page() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let doc_handle = DOCUMENTS.insert(doc);

        let page_handle = fz_load_page(0, doc_handle, 0);
        let kept = fz_keep_page(0, page_handle);
        assert_eq!(kept, page_handle);

        fz_drop_page(0, page_handle);
        fz_drop_document(0, doc_handle);
    }

    #[test]
    fn test_bound_page() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let doc_handle = DOCUMENTS.insert(doc);

        let page_handle = fz_load_page(0, doc_handle, 0);
        let bounds = fz_bound_page(0, page_handle);

        // Default US Letter size
        assert!((bounds.x1 - 612.0).abs() < 1.0);
        assert!((bounds.y1 - 792.0).abs() < 1.0);

        fz_drop_page(0, page_handle);
        fz_drop_document(0, doc_handle);
    }

    #[test]
    fn test_bound_page_invalid() {
        let bounds = fz_bound_page(0, 0);
        assert_eq!(bounds.x0, 0.0);
        assert_eq!(bounds.y0, 0.0);
        assert_eq!(bounds.x1, 0.0);
        assert_eq!(bounds.y1, 0.0);
    }

    // ============================================================================
    // Outline Tests
    // ============================================================================

    #[test]
    fn test_load_outline() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let doc_handle = DOCUMENTS.insert(doc);

        let outline_handle = fz_load_outline(0, doc_handle);
        assert_ne!(outline_handle, 0);

        crate::ffi::outline::fz_drop_outline(0, outline_handle);
        fz_drop_document(0, doc_handle);
    }

    #[test]
    fn test_load_outline_invalid_doc() {
        let outline_handle = fz_load_outline(0, 0);
        assert_eq!(outline_handle, 0);
    }

    // ============================================================================
    // Link Resolution Tests
    // ============================================================================

    #[test]
    fn test_resolve_link() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let doc_handle = DOCUMENTS.insert(doc);

        let mut x: f32 = 0.0;
        let mut y: f32 = 0.0;

        // Test #page=N format
        let result = fz_resolve_link(0, doc_handle, c"#page=5".as_ptr(), &mut x, &mut y);
        assert_eq!(result, 5);

        // Test #N format
        let result2 = fz_resolve_link(0, doc_handle, c"#10".as_ptr(), &mut x, &mut y);
        assert_eq!(result2, 10);

        // Test plain number
        let result3 = fz_resolve_link(0, doc_handle, c"3".as_ptr(), &mut x, &mut y);
        assert_eq!(result3, 3);

        fz_drop_document(0, doc_handle);
    }

    #[test]
    fn test_resolve_link_invalid() {
        // Null URI
        let result1 = fz_resolve_link(
            0,
            0,
            std::ptr::null(),
            std::ptr::null_mut(),
            std::ptr::null_mut(),
        );
        assert_eq!(result1, -1);

        // Invalid doc
        let result2 = fz_resolve_link(
            0,
            0,
            c"#page=1".as_ptr(),
            std::ptr::null_mut(),
            std::ptr::null_mut(),
        );
        assert_eq!(result2, -1);
    }

    #[test]
    fn test_resolve_link_invalid_uri() {
        let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
        let doc = Document::new(pdf_data.to_vec());
        let doc_handle = DOCUMENTS.insert(doc);

        let result = fz_resolve_link(
            0,
            doc_handle,
            c"invalid".as_ptr(),
            std::ptr::null_mut(),
            std::ptr::null_mut(),
        );
        assert_eq!(result, -1);

        fz_drop_document(0, doc_handle);
    }

    #[test]
    fn test_make_location_uri() {
        let mut buf = [0i8; 32];
        let result = fz_make_location_uri(0, 0, 5, buf.as_mut_ptr(), 32);
        assert!(!result.is_null());

        // Check the generated URI
        let uri = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) };
        assert_eq!(uri.to_str().unwrap(), "#page=5");
    }

    #[test]
    fn test_make_location_uri_null_buffer() {
        let result = fz_make_location_uri(0, 0, 5, std::ptr::null_mut(), 32);
        assert!(result.is_null());
    }
}
