//! Bookmark Writer - Create and modify PDF outline structures
//!
//! This module provides functionality to create and modify bookmark (outline)
//! structures in PDF files by generating the necessary PDF objects.

use super::bookmarks::Bookmark;
use super::error::{EnhancedError, Result};
use std::collections::HashMap;

/// Find a byte pattern in data, starting from a given position
fn find_bytes_pattern(data: &[u8], pattern: &[u8], start: usize) -> Option<usize> {
    if start >= data.len() || pattern.is_empty() {
        return None;
    }
    data[start..]
        .windows(pattern.len())
        .position(|window| window == pattern)
        .map(|idx| start + idx)
}

/// Find the closing `>>` of a PDF dictionary that starts at or after `start`.
/// Tracks `<<`/`>>` nesting depth so nested dicts are skipped correctly.
fn find_dict_end(data: &[u8], start: usize) -> Option<usize> {
    let mut depth: i32 = 0;
    let mut i = start;
    while i + 1 < data.len() {
        if data[i] == b'<' && data[i + 1] == b'<' {
            depth += 1;
            i += 2;
        } else if data[i] == b'>' && data[i + 1] == b'>' {
            depth -= 1;
            if depth <= 0 {
                return Some(i);
            }
            i += 2;
        } else {
            i += 1;
        }
    }
    None
}

/// Find a byte pattern in data, searching from the end
fn find_bytes_pattern_reverse(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
}

/// Bookmark outline item in PDF format
#[derive(Debug, Clone)]
pub struct OutlineItem {
    /// Object number for this outline item
    pub obj_num: i32,
    /// Title of the bookmark
    pub title: String,
    /// Destination page (0-indexed)
    pub page: usize,
    /// Parent outline object number
    pub parent: Option<i32>,
    /// First child outline object number
    pub first: Option<i32>,
    /// Last child outline object number
    pub last: Option<i32>,
    /// Next sibling outline object number
    pub next: Option<i32>,
    /// Previous sibling outline object number
    pub prev: Option<i32>,
    /// Number of descendants (for Count)
    pub count: i32,
}

impl OutlineItem {
    /// Create a new outline item
    pub fn new(obj_num: i32, title: String, page: usize) -> Self {
        Self {
            obj_num,
            title,
            page,
            parent: None,
            first: None,
            last: None,
            next: None,
            prev: None,
            count: 0,
        }
    }

    /// Generate PDF object for this outline item
    pub fn to_pdf_object(&self, page_obj_num: i32) -> String {
        let mut obj = format!("{} 0 obj\n<<\n", self.obj_num);
        obj.push_str(&format!("/Title ({})\n", escape_pdf_string(&self.title)));

        // Destination: [page_obj /XYZ null null null]
        obj.push_str(&format!(
            "/Dest [{} 0 R /XYZ null null null]\n",
            page_obj_num
        ));

        if let Some(parent) = self.parent {
            obj.push_str(&format!("/Parent {} 0 R\n", parent));
        }

        if let Some(first) = self.first {
            obj.push_str(&format!("/First {} 0 R\n", first));
        }

        if let Some(last) = self.last {
            obj.push_str(&format!("/Last {} 0 R\n", last));
        }

        if let Some(next) = self.next {
            obj.push_str(&format!("/Next {} 0 R\n", next));
        }

        if let Some(prev) = self.prev {
            obj.push_str(&format!("/Prev {} 0 R\n", prev));
        }

        if self.count != 0 {
            // Negative count means closed/collapsed by default (PDF spec section 8.2.2)
            obj.push_str(&format!("/Count -{}\n", self.count));
        }

        obj.push_str(">>\nendobj\n");
        obj
    }
}

/// Outline (bookmarks) root object
#[derive(Debug, Clone)]
pub struct Outlines {
    /// Object number for outlines root
    pub obj_num: i32,
    /// First outline item
    pub first: Option<i32>,
    /// Last outline item
    pub last: Option<i32>,
    /// Total count of visible outline items
    pub count: i32,
}

impl Outlines {
    /// Create new outlines root
    pub fn new(obj_num: i32) -> Self {
        Self {
            obj_num,
            first: None,
            last: None,
            count: 0,
        }
    }

    /// Generate PDF object for outlines root
    pub fn to_pdf_object(&self) -> String {
        let mut obj = format!("{} 0 obj\n<<\n", self.obj_num);
        obj.push_str("/Type /Outlines\n");

        if let Some(first) = self.first {
            obj.push_str(&format!("/First {} 0 R\n", first));
        }

        if let Some(last) = self.last {
            obj.push_str(&format!("/Last {} 0 R\n", last));
        }

        if self.count != 0 {
            obj.push_str(&format!("/Count {}\n", self.count));
        }

        obj.push_str(">>\nendobj\n");
        obj
    }
}

/// Escape PDF string
fn escape_pdf_string(s: &str) -> String {
    let mut result = String::new();
    for ch in s.chars() {
        match ch {
            '(' => result.push_str("\\("),
            ')' => result.push_str("\\)"),
            '\\' => result.push_str("\\\\"),
            '\n' => result.push_str("\\n"),
            '\r' => result.push_str("\\r"),
            '\t' => result.push_str("\\t"),
            _ if ch.is_ascii() => result.push(ch),
            _ => {
                // Unicode characters - use hex notation
                for byte in ch.to_string().as_bytes() {
                    result.push_str(&format!("\\{:03o}", byte));
                }
            }
        }
    }
    result
}

/// Build outline tree from a hierarchical bookmark list.
///
/// Returns the Outlines root object and a flat list of all OutlineItems
/// with correct parent/first/last/next/prev/count linkage.
pub fn build_outline_tree(
    bookmarks: &[Bookmark],
    start_obj_num: i32,
    page_objects: &HashMap<usize, i32>,
) -> Result<(Outlines, Vec<OutlineItem>)> {
    if bookmarks.is_empty() {
        return Err(EnhancedError::InvalidParameter(
            "No bookmarks provided".into(),
        ));
    }

    let outlines_obj_num = start_obj_num;
    let mut current_obj_num = start_obj_num + 1;
    let mut outline_items = Vec::new();
    let mut top_level_indices = Vec::new();

    for bookmark in bookmarks {
        let page_key = bookmark.page + 1;
        let _ = page_objects.get(&page_key).ok_or_else(|| {
            EnhancedError::InvalidParameter(format!(
                "Page {} not found (1-indexed: {})",
                bookmark.page, page_key
            ))
        })?;

        let item_obj_num = current_obj_num;
        current_obj_num += 1;

        let mut item = OutlineItem::new(item_obj_num, bookmark.title.clone(), bookmark.page);
        item.parent = Some(outlines_obj_num);

        let item_idx = outline_items.len();
        top_level_indices.push(item_idx);

        if !bookmark.children.is_empty() {
            let (child_items, first_child, last_child, descendant_count) =
                build_child_items_linked(
                    &bookmark.children,
                    &mut current_obj_num,
                    item_obj_num,
                    page_objects,
                )?;
            item.first = Some(first_child);
            item.last = Some(last_child);
            item.count = descendant_count;
            outline_items.push(item);
            outline_items.extend(child_items);
        } else {
            outline_items.push(item);
        }
    }

    // Link top-level siblings
    for w in top_level_indices.windows(2) {
        let (ci, ni) = (w[0], w[1]);
        let next_obj = outline_items[ni].obj_num;
        let curr_obj = outline_items[ci].obj_num;
        outline_items[ci].next = Some(next_obj);
        outline_items[ni].prev = Some(curr_obj);
    }

    let first_top = top_level_indices[0];
    let last_top = *top_level_indices.last().unwrap();

    let mut outlines = Outlines::new(outlines_obj_num);
    outlines.first = Some(outline_items[first_top].obj_num);
    outlines.last = Some(outline_items[last_top].obj_num);
    outlines.count = outline_items.len() as i32;

    Ok((outlines, outline_items))
}

/// Build child outline items recursively, returning properly-linked items.
///
/// Returns (flat_items, first_child_obj_num, last_child_obj_num, total_descendant_count).
fn build_child_items_linked(
    children: &[Bookmark],
    current_obj_num: &mut i32,
    parent_obj_num: i32,
    page_objects: &HashMap<usize, i32>,
) -> Result<(Vec<OutlineItem>, i32, i32, i32)> {
    let mut items = Vec::new();
    let mut child_indices = Vec::new();

    for child in children {
        let page_key = child.page + 1;
        let _ = page_objects.get(&page_key).ok_or_else(|| {
            EnhancedError::InvalidParameter(format!(
                "Page {} not found (1-indexed: {})",
                child.page, page_key
            ))
        })?;

        let item_obj_num = *current_obj_num;
        *current_obj_num += 1;

        let mut item = OutlineItem::new(item_obj_num, child.title.clone(), child.page);
        item.parent = Some(parent_obj_num);

        let item_idx = items.len();
        child_indices.push(item_idx);

        if !child.children.is_empty() {
            let (grandchild_items, first_gc, last_gc, gc_count) = build_child_items_linked(
                &child.children,
                current_obj_num,
                item_obj_num,
                page_objects,
            )?;
            item.first = Some(first_gc);
            item.last = Some(last_gc);
            item.count = gc_count;
            items.push(item);
            items.extend(grandchild_items);
        } else {
            items.push(item);
        }
    }

    // Link siblings at this level
    for w in child_indices.windows(2) {
        let (ci, ni) = (w[0], w[1]);
        let next_obj = items[ni].obj_num;
        let curr_obj = items[ci].obj_num;
        items[ci].next = Some(next_obj);
        items[ni].prev = Some(curr_obj);
    }

    let first_child_obj = items[child_indices[0]].obj_num;
    let last_child_obj = items[*child_indices.last().unwrap()].obj_num;
    let total_count = items.len() as i32;

    Ok((items, first_child_obj, last_child_obj, total_count))
}

/// Find the position where the xref section (traditional or stream) begins.
/// Works with both traditional xref tables and cross-reference streams.
fn find_xref_start(pdf_data: &[u8]) -> Result<usize> {
    // Try traditional xref table first
    if let Some(pos) = find_bytes_pattern_reverse(pdf_data, b"\nxref") {
        return Ok(pos + 1);
    }
    if let Some(pos) = find_bytes_pattern_reverse(pdf_data, b"\r\nxref") {
        return Ok(pos + 2);
    }
    // Check for xref at the very start of the file or standalone
    if let Some(pos) = find_bytes_pattern_reverse(pdf_data, b"xref") {
        if pos == 0 || pdf_data[pos - 1] == b'\n' || pdf_data[pos - 1] == b'\r' {
            return Ok(pos);
        }
    }

    // Cross-reference stream: parse startxref to find the xref stream object
    let startxref_pos = find_bytes_pattern_reverse(pdf_data, b"startxref")
        .ok_or_else(|| EnhancedError::Generic("Neither xref table nor startxref found".into()))?;

    let after = &pdf_data[startxref_pos + 9..];
    let num_str: String = after
        .iter()
        .skip_while(|b| b.is_ascii_whitespace())
        .take_while(|b| b.is_ascii_digit())
        .map(|b| *b as char)
        .collect();

    let xref_offset = num_str
        .parse::<usize>()
        .map_err(|_| EnhancedError::Generic(format!("Invalid startxref value: '{}'", num_str)))?;

    if xref_offset >= pdf_data.len() {
        return Err(EnhancedError::Generic(format!(
            "startxref offset {} exceeds file size {}",
            xref_offset,
            pdf_data.len()
        )));
    }

    // Verify this points to an object (xref stream) rather than a traditional xref
    if pdf_data[xref_offset..].starts_with(b"xref") {
        return Ok(xref_offset);
    }

    // It's a cross-reference stream object — scan backward from this position
    // to find the start of the line (the "N 0 obj" header).
    // Cap at 1000 bytes to avoid runaway scans on malformed data.
    let mut line_start = xref_offset;
    let scan_limit = line_start.saturating_sub(1000);
    while line_start > scan_limit
        && pdf_data[line_start - 1] != b'\n'
        && pdf_data[line_start - 1] != b'\r'
    {
        line_start -= 1;
    }

    Ok(line_start)
}

/// Add bookmarks to PDF data
pub fn insert_bookmarks_into_pdf(
    pdf_data: &mut Vec<u8>,
    bookmarks: &[Bookmark],
    page_objects: &HashMap<usize, i32>,
    next_obj_num: i32,
) -> Result<i32> {
    let (outlines, items) = build_outline_tree(bookmarks, next_obj_num, page_objects)?;

    let mut catalog_obj_num = find_catalog_obj_num(pdf_data)?;

    let catalog_in_raw = find_bytes_pattern(pdf_data, b"/Type /Catalog", 0)
        .or_else(|| find_bytes_pattern(pdf_data, b"/Type/Catalog", 0))
        .is_some();

    let existing_outlines_obj_start = find_outline_object_start(pdf_data);
    let xref_pos = find_xref_start(pdf_data)?;

    let mut truncate_pos = if let Some(outlines_start) = existing_outlines_obj_start {
        // Only truncate at old outlines if the region between outlines and xref contains
        // no page objects — flattened PDFs may have pages interleaved with outlines.
        let between = &pdf_data[outlines_start..xref_pos];
        let has_page_objects = between
            .windows(10)
            .any(|w| w == b"/Type/Page" || w.starts_with(b"/Type /Pag"));
        if has_page_objects {
            xref_pos
        } else {
            outlines_start
        }
    } else {
        xref_pos
    };

    let mut extra_objects = String::new();
    let mut actual_next_obj = next_obj_num;

    if catalog_in_raw {
        // Catalog is visible in raw text — modify it in place
        let mut bytes_inserted: i64 = 0;
        let catalog_start = find_bytes_pattern(pdf_data, b"/Type /Catalog", 0)
            .or_else(|| find_bytes_pattern(pdf_data, b"/Type/Catalog", 0));

        if let Some(catalog_pos) = catalog_start {
            // Find the `<<` that opens the outermost Catalog dict.
            // Search backwards for "obj" to find the object start, then forward for "<<"
            let obj_start = pdf_data[..catalog_pos]
                .windows(3)
                .rposition(|w| w == b"obj")
                .unwrap_or(catalog_pos.saturating_sub(50));
            let dict_start = pdf_data[obj_start..catalog_pos]
                .windows(2)
                .position(|w| w == b"<<")
                .map(|p| obj_start + p)
                .unwrap_or(catalog_pos.saturating_sub(2));
            if let Some(dict_end_offset) = find_dict_end(pdf_data, dict_start) {
                // Search the FULL dict (from dict_start+2 to dict_end) for /Outlines.
                // Use multiple patterns to catch whitespace variations: /Outlines , /Outlines\n, /Outlines\r
                let search_base = dict_start + 2;
                let catalog_section = &pdf_data[search_base..dict_end_offset];
                let outlines_pos = find_bytes_pattern(catalog_section, b"/Outlines ", 0)
                    .or_else(|| find_bytes_pattern(catalog_section, b"/Outlines\n", 0))
                    .or_else(|| find_bytes_pattern(catalog_section, b"/Outlines\r", 0))
                    .or_else(|| find_bytes_pattern(catalog_section, b"/Outlines\t", 0))
                    .or_else(|| find_bytes_pattern(catalog_section, b"/Outlines", 0));
                if let Some(outlines_pos) = outlines_pos {
                    let existing_pos = search_base + outlines_pos;
                    let search_start = existing_pos + 9;
                    let mut ref_end = search_start;
                    while ref_end < pdf_data.len() && pdf_data[ref_end].is_ascii_whitespace() {
                        ref_end += 1;
                    }
                    while ref_end < pdf_data.len() && pdf_data[ref_end].is_ascii_digit() {
                        ref_end += 1;
                    }
                    while ref_end < pdf_data.len()
                        && (pdf_data[ref_end].is_ascii_whitespace()
                            || pdf_data[ref_end] == b'0'
                            || pdf_data[ref_end] == b'R')
                    {
                        ref_end += 1;
                        if ref_end > 0 && pdf_data[ref_end - 1] == b'R' {
                            break;
                        }
                    }
                    let new_ref = format!("/Outlines {} 0 R", outlines.obj_num);
                    let old_len = ref_end - existing_pos;
                    let new_bytes = new_ref.as_bytes();
                    bytes_inserted = new_bytes.len() as i64 - old_len as i64;
                    pdf_data.splice(existing_pos..ref_end, new_bytes.iter().cloned());
                } else {
                    let outlines_ref = format!("/Outlines {} 0 R\n", outlines.obj_num);
                    bytes_inserted = outlines_ref.len() as i64;
                    let outlines_bytes = outlines_ref.as_bytes();
                    pdf_data.splice(
                        dict_end_offset..dict_end_offset,
                        outlines_bytes.iter().cloned(),
                    );
                }
            }
        }
        if bytes_inserted != 0 {
            truncate_pos = ((truncate_pos as i64) + bytes_inserted) as usize;
        }
    } else {
        // Catalog is inside a compressed ObjStm — create a new standalone Catalog
        let new_catalog_obj_num = actual_next_obj;
        actual_next_obj += 1;

        let pages_ref = find_root_pages_ref(pdf_data);
        let pages_obj = pages_ref.unwrap_or(1);

        extra_objects.push_str(&format!(
            "{} 0 obj\n<</Type /Catalog/Pages {} 0 R/Outlines {} 0 R>>\nendobj\n",
            new_catalog_obj_num, pages_obj, outlines.obj_num
        ));
        catalog_obj_num = new_catalog_obj_num;
    }

    // Generate outline objects
    let mut outline_data = String::new();
    outline_data.push_str(&extra_objects);
    outline_data.push_str(&outlines.to_pdf_object());
    outline_data.push('\n');

    for item in &items {
        let page_key = item.page + 1;
        let page_obj = *page_objects.get(&page_key).ok_or_else(|| {
            EnhancedError::InvalidParameter(format!(
                "Page {} not found in page_objects (1-indexed: {})",
                item.page, page_key
            ))
        })?;
        outline_data.push_str(&item.to_pdf_object(page_obj));
        outline_data.push('\n');
    }

    if truncate_pos < pdf_data.len() {
        pdf_data.truncate(truncate_pos);
    }

    pdf_data.extend_from_slice(outline_data.as_bytes());

    let new_obj_count = actual_next_obj + 1 + items.len() as i32;

    // Rebuild xref table by scanning for all "N 0 obj" patterns
    let mut offsets = vec![0usize; new_obj_count as usize];

    let mut search_pos = 0;
    while search_pos < pdf_data.len() {
        if search_pos == 0 || pdf_data[search_pos - 1] == b'\n' || pdf_data[search_pos - 1] == b'\r'
        {
            if pdf_data[search_pos].is_ascii_digit() {
                let num_start = search_pos;
                let mut num_end = search_pos;
                while num_end < pdf_data.len() && pdf_data[num_end].is_ascii_digit() {
                    num_end += 1;
                }

                if num_end + 6 <= pdf_data.len() && &pdf_data[num_end..num_end + 6] == b" 0 obj" {
                    if let Ok(obj_num_str) = std::str::from_utf8(&pdf_data[num_start..num_end]) {
                        if let Ok(obj_num) = obj_num_str.parse::<i32>() {
                            if obj_num > 0 && (obj_num as usize) < offsets.len() {
                                offsets[obj_num as usize] = search_pos;
                            }
                        }
                    }
                    search_pos = num_end + 6;
                    continue;
                }
            }
        }
        search_pos += 1;
    }

    // Write new traditional xref table (compatible with all PDF readers)
    let xref_offset = pdf_data.len();
    pdf_data.extend_from_slice(format!("xref\n0 {}\n", new_obj_count).as_bytes());
    pdf_data.extend_from_slice(b"0000000000 65535 f \n");

    for i in 1..new_obj_count as usize {
        let offset = offsets.get(i).copied().unwrap_or(0);
        pdf_data.extend_from_slice(format!("{:010} 00000 n \n", offset).as_bytes());
    }

    pdf_data.extend_from_slice(
        format!(
            "trailer\n<</Size {}/Root {} 0 R>>\n",
            new_obj_count, catalog_obj_num
        )
        .as_bytes(),
    );
    pdf_data.extend_from_slice(format!("startxref\n{}\n%%EOF\n", xref_offset).as_bytes());

    Ok(new_obj_count)
}

/// Find the byte position of an existing /Type /Outlines object's "N 0 obj" header.
fn find_outline_object_start(pdf_data: &[u8]) -> Option<usize> {
    let outlines_pos = find_bytes_pattern(pdf_data, b"/Type /Outlines", 0)
        .or_else(|| find_bytes_pattern(pdf_data, b"/Type/Outlines", 0))?;

    let search_start = outlines_pos.saturating_sub(200);
    let region = &pdf_data[search_start..outlines_pos];
    let mut last_obj_start = None;
    for i in 0..region.len().saturating_sub(6) {
        if &region[i..i + 6] == b" 0 obj" {
            let mut num_end = i;
            let mut num_start = num_end;
            while num_start > 0 && region[num_start - 1].is_ascii_digit() {
                num_start -= 1;
            }
            if num_start < num_end {
                // Accept any non-digit boundary before the object number
                last_obj_start = Some(search_start + num_start);
            }
        }
    }
    last_obj_start
}

/// Find the root /Pages object number by decompressing ObjStm if needed.
fn find_root_pages_ref(pdf_data: &[u8]) -> Option<i32> {
    // First check raw text for Catalog → /Pages reference
    let content = String::from_utf8_lossy(pdf_data);
    for pat in ["/Type /Catalog", "/Type/Catalog"] {
        if let Some(cat_pos) = content.find(pat) {
            let end = {
                let mut e = (cat_pos + 1024).min(content.len());
                while e > cat_pos && !content.is_char_boundary(e) {
                    e -= 1;
                }
                e
            };
            let region = &content[cat_pos..end];
            if let Some(pages_pos) = region.find("/Pages") {
                let after_pages = &region[pages_pos + 6..];
                let num: String = after_pages
                    .chars()
                    .skip_while(|c| c.is_ascii_whitespace())
                    .take_while(|c| c.is_ascii_digit())
                    .collect();
                if let Ok(n) = num.parse::<i32>() {
                    return Some(n);
                }
            }
        }
    }

    // Decompress ObjStm to find the Catalog
    match crate::enhanced::object_stream::parse_all_object_streams(pdf_data) {
        Ok(all_objects) => {
            for data in all_objects.values() {
                if data.contains("/Type /Catalog") || data.contains("/Type/Catalog") {
                    if let Some(pages_pos) = data.find("/Pages") {
                        let after_pages = &data[pages_pos + 6..];
                        let num: String = after_pages
                            .chars()
                            .skip_while(|c| c.is_ascii_whitespace())
                            .take_while(|c| c.is_ascii_digit())
                            .collect();
                        if let Ok(n) = num.parse::<i32>() {
                            return Some(n);
                        }
                    }
                }
            }
        }
        Err(e) => {
            eprintln!("Warning: failed to parse object streams: {:?}", e);
        }
    }

    None
}

/// Extract the catalog object number from the PDF's existing trailer.
fn find_catalog_obj_num(pdf_data: &[u8]) -> Result<i32> {
    // Method 1: Search backwards for "trailer" then find /Root N 0 R
    if let Some(trailer_pos) = find_bytes_pattern_reverse(pdf_data, b"trailer") {
        let trailer_region =
            &pdf_data[trailer_pos..std::cmp::min(trailer_pos + 512, pdf_data.len())];
        if let Some(root_offset) = find_bytes_pattern(trailer_region, b"/Root", 0) {
            let after_root = &trailer_region[root_offset + 5..];
            let trimmed = after_root
                .iter()
                .skip_while(|b| b.is_ascii_whitespace())
                .cloned()
                .collect::<Vec<u8>>();
            let num_bytes: Vec<u8> = trimmed
                .iter()
                .take_while(|b| b.is_ascii_digit())
                .cloned()
                .collect();
            if !num_bytes.is_empty() {
                if let Ok(s) = std::str::from_utf8(&num_bytes) {
                    if let Ok(n) = s.parse::<i32>() {
                        return Ok(n);
                    }
                }
            }
        }
    }

    // Method 2: xref stream — look for /Type /XRef with /Root
    let content = String::from_utf8_lossy(pdf_data);
    for xref_pat in ["/Type /XRef", "/Type/XRef"] {
        if let Some(xref_pos) = content.rfind(xref_pat) {
            let start = xref_pos.saturating_sub(2048);
            let end = (xref_pos + 512).min(content.len());
            let mut safe_end = end;
            while safe_end > start && !content.is_char_boundary(safe_end) {
                safe_end -= 1;
            }
            let mut safe_start = start;
            while safe_start < safe_end && !content.is_char_boundary(safe_start) {
                safe_start += 1;
            }
            let region = content[safe_start..safe_end].as_bytes();
            if let Some(root_off) = find_bytes_pattern(region, b"/Root", 0) {
                let after_root = &region[root_off + 5..];
                let trimmed: Vec<u8> = after_root
                    .iter()
                    .skip_while(|b| b.is_ascii_whitespace())
                    .cloned()
                    .collect();
                let num_bytes: Vec<u8> = trimmed
                    .iter()
                    .take_while(|b| b.is_ascii_digit())
                    .cloned()
                    .collect();
                if !num_bytes.is_empty() {
                    if let Ok(s) = std::str::from_utf8(&num_bytes) {
                        if let Ok(n) = s.parse::<i32>() {
                            return Ok(n);
                        }
                    }
                }
            }
        }
    }

    // Method 3: find /Type /Catalog and work backwards to get its object number
    let catalog_pos = find_bytes_pattern(pdf_data, b"/Type /Catalog", 0)
        .or_else(|| find_bytes_pattern(pdf_data, b"/Type/Catalog", 0));

    if let Some(catalog_pos) = catalog_pos {
        let before = &pdf_data[..catalog_pos];
        if let Some(obj_pos) = find_bytes_pattern_reverse(before, b" 0 obj") {
            let mut num_end = obj_pos;
            let mut num_start = num_end;
            while num_start > 0 && pdf_data[num_start - 1].is_ascii_digit() {
                num_start -= 1;
            }
            if num_start < num_end {
                if let Ok(s) = std::str::from_utf8(&pdf_data[num_start..num_end]) {
                    if let Ok(n) = s.parse::<i32>() {
                        return Ok(n);
                    }
                }
            }
        }
    }

    // Method 4: decompress ObjStm to find Catalog
    match crate::enhanced::object_stream::parse_all_object_streams(pdf_data) {
        Ok(all_objects) => {
            for (&obj_num, obj_data) in &all_objects {
                if obj_data.contains("/Type /Catalog") || obj_data.contains("/Type/Catalog") {
                    return Ok(obj_num);
                }
            }
        }
        Err(e) => {
            eprintln!("Warning: failed to parse object streams: {:?}", e);
        }
    }

    Err(EnhancedError::Generic(
        "Could not determine catalog object number".into(),
    ))
}
