//! Overlay Merging - Merge overlay PDFs onto target PDF pages
//!
//! This module provides functionality to merge overlay PDFs (like redaction or highlight overlays)
//! onto specific pages of a target PDF.

use super::error::{EnhancedError, Result};
use super::pdf_reader::PdfDocument;
use super::writer::PdfWriter;
use crate::pdf::object::{Array, Dict, Name, Object};
use std::collections::HashMap;
use std::fs;
use std::path::Path;

/// Merge an overlay PDF onto specific pages of a target PDF
///
/// This merges the overlay PDF's content streams onto the target PDF pages.
///
/// # Arguments
/// * `target_path` - Path to target PDF
/// * `overlay_path` - Path to overlay PDF (single page or multi-page)
/// * `output_path` - Path to output PDF
/// * `pages` - Page numbers to apply overlay to (0-indexed, empty = all pages)
pub fn merge_overlay(
    target_path: &str,
    overlay_path: &str,
    output_path: &str,
    pages: &[usize],
) -> Result<()> {
    if !Path::new(target_path).exists() {
        return Err(EnhancedError::Io(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            format!("Target PDF not found: {}", target_path),
        )));
    }

    if !Path::new(overlay_path).exists() {
        return Err(EnhancedError::Io(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            format!("Overlay PDF not found: {}", overlay_path),
        )));
    }

    // Parse both PDFs
    let target_doc = PdfDocument::open(target_path)?;
    let overlay_doc = PdfDocument::open(overlay_path)?;

    let target_page_count = target_doc.page_count()?;
    // Build page object list from PdfDocument (handles ObjStm)
    let mut target_page_objs: Vec<i32> = Vec::new();
    for i in 0..target_page_count {
        match target_doc.get_page_object(i) {
            Some(obj) => target_page_objs.push(obj),
            None => eprintln!("Warning: could not resolve page object for page {}", i),
        }
    }
    let overlay_page_count = overlay_doc.page_count()?;

    // Determine which pages to process
    let pages_to_process: Vec<usize> = if pages.is_empty() {
        (0..target_page_count).collect()
    } else {
        pages.to_vec()
    };

    // Validate page numbers
    for &page_num in &pages_to_process {
        if page_num >= target_page_count {
            return Err(EnhancedError::InvalidParameter(format!(
                "Page {} does not exist in target PDF (has {} pages)",
                page_num, target_page_count
            )));
        }
    }

    // Read overlay PDF data and extract streams + ExtGState resources
    let overlay_data = fs::read(overlay_path)?;
    let overlay_streams: Vec<(usize, Vec<u8>, HashMap<String, String>)> = {
        let mut streams = Vec::new();
        for &page_num in &pages_to_process {
            let overlay_page_num = if overlay_page_count == 1 {
                0
            } else {
                page_num.min(overlay_page_count - 1)
            };
            let stream = extract_page_content_stream(&overlay_data, overlay_page_num)?;
            let ext_g_states = extract_page_ext_g_states(&overlay_data, overlay_page_num);
            streams.push((page_num, stream, ext_g_states));
        }
        streams
    };

    // Read target PDF data
    let mut target_data = fs::read(target_path)?;

    // Preserve the Catalog object number from the original trailer before we
    // strip it. The Catalog is NOT always object 1 (e.g. PdfWriter puts it last).
    let catalog_obj_num = find_catalog_obj_num_from_bytes(&target_data);

    // First, find and remove xref/trailer/EOF - we'll rebuild it at the end
    remove_xref_trailer(&mut target_data);

    // Track the max object number as we add overlay objects
    let mut max_obj_num = find_max_object_number(&target_data);

    // Build indexed page objects list
    let page_objects_indexed: Vec<(usize, i32)> = target_page_objs
        .iter()
        .enumerate()
        .map(|(i, &obj)| (i, obj))
        .collect();

    // Merge each overlay into target (this only adds objects, doesn't rebuild xref)
    for (page_num, overlay_content_stream, ext_g_states) in overlay_streams {
        if overlay_content_stream.is_empty() {
            continue;
        }
        if let Ok(new_obj_num) = add_overlay_object(
            &mut target_data,
            page_num,
            &overlay_content_stream,
            &ext_g_states,
            max_obj_num,
            &page_objects_indexed,
        ) {
            max_obj_num = new_obj_num;
        }
    }

    // Now rebuild the xref table and trailer ONCE for all objects
    rebuild_xref_and_trailer(&mut target_data, max_obj_num + 1, catalog_obj_num)?;

    // Write modified PDF
    fs::write(output_path, target_data)?;

    Ok(())
}

/// Remove xref, trailer, and EOF from PDF data
fn remove_xref_trailer(data: &mut Vec<u8>) {
    // Try traditional xref (search from end for the last occurrence)
    if let Some(pos) = rfind_bytes_pattern(data, b"\nxref") {
        data.truncate(pos + 1);
        return;
    }
    if let Some(pos) = rfind_bytes_pattern(data, b"\r\nxref") {
        data.truncate(pos + 2);
        return;
    }

    // Loop to find the last real "xref" that isn't part of "startxref"
    {
        let mut search_end = data.len();
        loop {
            if let Some(pos) = rfind_bytes_pattern(&data[..search_end], b"xref") {
                if pos >= 5 && &data[pos - 5..pos] == b"start" {
                    search_end = pos;
                    continue;
                }
                data.truncate(pos);
                return;
            }
            break;
        }
    }

    // For xref streams: find startxref, parse the offset, and truncate at the xref stream object
    if let Some(startxref_pos) = rfind_bytes_pattern(data, b"startxref") {
        let after = &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();
        if let Ok(xref_offset) = num_str.parse::<usize>() {
            if xref_offset < data.len() {
                let mut line_start = xref_offset;
                let scan_limit = line_start.saturating_sub(200);
                while line_start > scan_limit
                    && line_start > 0
                    && data[line_start - 1] != b'\n'
                    && data[line_start - 1] != b'\r'
                {
                    line_start -= 1;
                }
                data.truncate(line_start);
                return;
            }
        }
        data.truncate(startxref_pos);
    }
}

/// Find the maximum object number in the PDF
pub(crate) fn find_max_object_number(data: &[u8]) -> i32 {
    let obj_pattern = b" 0 obj";
    let mut max_obj_num = 0;
    let mut search_pos = 0;

    while let Some(obj_pos) = find_bytes_pattern_from(data, obj_pattern, search_pos) {
        let before_obj = &data[..obj_pos];
        let obj_num = extract_number_backwards(before_obj);
        max_obj_num = max_obj_num.max(obj_num);
        search_pos = obj_pos + obj_pattern.len();
    }

    max_obj_num
}

/// Add an overlay object for a specific page (doesn't rebuild xref)
fn add_overlay_object(
    target_data: &mut Vec<u8>,
    page_num: usize,
    overlay_content: &[u8],
    ext_g_states: &HashMap<String, String>,
    current_max_obj: i32,
    page_objects_list: &[(usize, i32)],
) -> Result<i32> {
    if page_num >= page_objects_list.len() {
        return Err(EnhancedError::Generic(format!(
            "Page {} doesn't exist in target (only {} pages)",
            page_num,
            page_objects_list.len()
        )));
    }

    let (_, page_obj_num) = page_objects_list[page_num];
    let new_obj_num = current_max_obj + 1;

    // Find the page object
    let obj_marker = format!("{} 0 obj", page_obj_num);
    let obj_marker_bytes = obj_marker.as_bytes();
    let obj_pos = find_bytes_pattern_from(target_data, obj_marker_bytes, 0)
        .ok_or_else(|| EnhancedError::Generic(format!("Page object {} not found", page_obj_num)))?;

    // Find /Contents in the page object section
    let page_section_end = (obj_pos + 2000).min(target_data.len());
    let page_section = &target_data[obj_pos..page_section_end];

    let contents_pattern = b"/Contents";
    let contents_pos = find_bytes_pattern_from(page_section, contents_pattern, 0)
        .ok_or_else(|| EnhancedError::Generic("No /Contents in page object".into()))?;

    // Parse the original content object number(s)
    let after_contents = &page_section[contents_pos + 9..];

    // Check if /Contents is already an array
    let trimmed = after_contents
        .iter()
        .skip_while(|&&b| b == b' ')
        .cloned()
        .collect::<Vec<u8>>();

    let (original_content_refs, is_array) = if trimmed.starts_with(b"[") {
        // Already an array - extract all references
        let mut refs = Vec::new();
        let mut i = 1; // Skip '['
        while i < trimmed.len() {
            if trimmed[i] == b']' {
                break;
            }
            if trimmed[i].is_ascii_digit() {
                let num = extract_first_number(&trimmed[i..]);
                if let Some(n) = num {
                    refs.push(n);
                    // Skip past this reference
                    while i < trimmed.len()
                        && (trimmed[i].is_ascii_digit()
                            || trimmed[i] == b' '
                            || trimmed[i] == b'0'
                            || trimmed[i] == b'R')
                    {
                        i += 1;
                    }
                    continue;
                }
            }
            i += 1;
        }
        (refs, true)
    } else {
        // Single reference
        let original_content_obj = extract_first_number(after_contents)
            .ok_or_else(|| EnhancedError::Generic("Invalid /Contents reference".into()))?;
        (vec![original_content_obj], false)
    };

    // Create the new content stream object
    let overlay_str = String::from_utf8_lossy(overlay_content);
    let new_stream_obj = format!(
        "{} 0 obj\n<</Length {}>>\nstream\n{}\nendstream\nendobj\n",
        new_obj_num,
        overlay_content.len(),
        overlay_str
    );

    // Update the page's /Contents to include the new overlay reference
    if is_array {
        // Contents is already an array - find it and add to it
        let abs_contents_pos = obj_pos + contents_pos;
        let after_abs = &target_data[abs_contents_pos + 9..];
        if let Some(bracket_pos) = after_abs.iter().position(|&b| b == b'[') {
            if let Some(close_pos) = after_abs.iter().position(|&b| b == b']') {
                let insert_pos = abs_contents_pos + 9 + close_pos;
                let new_ref = format!(" {} 0 R", new_obj_num);
                let new_ref_bytes = new_ref.as_bytes();
                target_data.splice(insert_pos..insert_pos, new_ref_bytes.iter().copied());
            }
        }
    } else {
        // Single reference - convert to array
        let base_pattern = format!("/Contents {} 0 R", original_content_refs[0]);
        let base_bytes = base_pattern.as_bytes();

        if let Some(contents_pos) = find_bytes_pattern(target_data, base_bytes) {
            let pattern_end = contents_pos + base_bytes.len();
            let new_contents = format!(
                "/Contents [{} 0 R {} 0 R]",
                original_content_refs[0], new_obj_num
            );
            let new_bytes = new_contents.as_bytes();
            target_data.splice(contents_pos..pattern_end, new_bytes.iter().copied());
        }
    }

    // Append the new object to the end of the PDF
    target_data.extend_from_slice(new_stream_obj.as_bytes());

    // Add ExtGState resources for transparency if needed
    if !ext_g_states.is_empty() {
        add_ext_g_states_to_page(target_data, page_obj_num, ext_g_states);
    }

    Ok(new_obj_num)
}

/// Extract the Catalog (/Root) object number from raw PDF bytes by parsing the trailer.
/// Falls back to scanning for `/Type /Catalog` if the trailer cannot be parsed.
pub(crate) fn find_catalog_obj_num_from_bytes(data: &[u8]) -> i32 {
    // Method 1: traditional trailer → /Root N 0 R
    if let Some(trailer_pos) = rfind_bytes_pattern(data, b"trailer") {
        let region_end = (trailer_pos + 512).min(data.len());
        let region = &data[trailer_pos..region_end];
        if let Some(n) = extract_root_ref(region) {
            return n;
        }
    }

    // Method 2: cross-reference stream (/Type /XRef or /Type/XRef) → /Root N 0 R
    let content = String::from_utf8_lossy(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 region = content[start..std::cmp::min(xref_pos + 512, content.len())].as_bytes();
            if let Some(n) = extract_root_ref(region) {
                return n;
            }
        }
    }

    // Method 3: find /Type /Catalog in raw text
    let catalog_patterns = ["/Type /Catalog", "/Type/Catalog"];
    for pat in catalog_patterns {
        if let Some(pos) = content.find(pat) {
            let before = &content[..pos];
            if let Some(obj_pos) = before.rfind(" 0 obj") {
                let before_obj = &before[..obj_pos];
                let num_str: String = before_obj
                    .chars()
                    .rev()
                    .take_while(|c| c.is_ascii_digit())
                    .collect::<String>()
                    .chars()
                    .rev()
                    .collect();
                if let Ok(n) = num_str.parse::<i32>() {
                    return n;
                }
            }
        }
    }

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

    1 // last resort
}

fn extract_root_ref(region: &[u8]) -> Option<i32> {
    let root_pos = find_bytes_pattern_from(region, b"/Root", 0)?;
    let after_root = &region[root_pos + 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() {
        return None;
    }
    let s = std::str::from_utf8(&num_bytes).ok()?;
    s.parse::<i32>().ok()
}

/// Rebuild xref table and trailer (called once after all modifications)
pub(crate) fn rebuild_xref_and_trailer(
    data: &mut Vec<u8>,
    obj_count: i32,
    catalog_obj_num: i32,
) -> Result<()> {
    // Collect all object offsets
    let mut offsets = vec![0usize; obj_count as usize];

    // Find all "N 0 obj" patterns
    let mut search_pos = 0;
    while search_pos < data.len() {
        if search_pos == 0 || data[search_pos - 1] == b'\n' || data[search_pos - 1] == b'\r' {
            if data[search_pos].is_ascii_digit() {
                let num_start = search_pos;
                let mut num_end = search_pos;
                while num_end < data.len() && data[num_end].is_ascii_digit() {
                    num_end += 1;
                }

                if num_end + 6 <= data.len() {
                    let after_num = &data[num_end..num_end + 6];
                    if after_num == b" 0 obj" {
                        if let Ok(obj_num_str) = std::str::from_utf8(&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 xref table
    let xref_offset = data.len();
    data.extend_from_slice(format!("xref\n0 {}\n", obj_count).as_bytes());
    data.extend_from_slice(b"0000000000 65535 f \n");

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

    // Write trailer with the ACTUAL Catalog object number
    data.extend_from_slice(
        format!(
            "trailer\n<</Size {}/Root {} 0 R>>\n",
            obj_count, catalog_obj_num
        )
        .as_bytes(),
    );
    data.extend_from_slice(format!("startxref\n{}\n%%EOF\n", xref_offset).as_bytes());

    Ok(())
}

/// Extract content stream from a page (using byte-level operations for safety)
fn extract_page_content_stream(data: &[u8], page_num: usize) -> Result<Vec<u8>> {
    // Find page object by looking for /Type /Page (not /Type /Pages)
    // Use byte-level operations to avoid UTF-8 boundary issues
    let type_page_pattern = b"/Type /Page";
    let type_pages_pattern = b"/Type /Pages";
    let obj_pattern = b" 0 obj";

    let mut page_objects: Vec<(usize, i32)> = Vec::new();
    let mut search_pos = 0;

    while let Some(type_pos) = find_bytes_pattern_from(data, type_page_pattern, search_pos) {
        // Check this isn't /Type /Pages
        if data[type_pos..].starts_with(type_pages_pattern) {
            search_pos = type_pos + type_pages_pattern.len();
            continue;
        }

        // Find the object number by looking back for " 0 obj"
        if let Some(obj_pos) = rfind_bytes_pattern(&data[..type_pos], obj_pattern) {
            // Find the number before " 0 obj"
            let before_obj = &data[..obj_pos];
            let obj_num = extract_number_backwards(before_obj);

            if obj_num > 0 {
                page_objects.push((type_pos, obj_num));
            }
        }

        search_pos = type_pos + type_page_pattern.len();
    }

    if page_num >= page_objects.len() {
        return Ok(Vec::new());
    }

    let (page_type_pos, page_obj_num) = page_objects[page_num];

    // Find the actual object boundaries for this page
    // First, find where the object starts by looking for "N 0 obj"
    let obj_marker = format!("{} 0 obj", page_obj_num);
    let obj_marker_bytes = obj_marker.as_bytes();

    let obj_start = find_bytes_pattern_from(data, obj_marker_bytes, 0);

    // Find where this object ends by looking for "endobj" after the object start
    let obj_end = if let Some(start) = obj_start {
        find_bytes_pattern_from(data, b"endobj", start)
            .map(|end| end + 6)
            .unwrap_or_else(|| (start + 2000).min(data.len()))
    } else {
        // Fallback to window around /Type /Page if we can't find the object marker
        (page_type_pos + 500).min(data.len())
    };

    let search_start = obj_start.unwrap_or_else(|| page_type_pos.saturating_sub(500));
    let search_end = obj_end;
    let page_section = &data[search_start..search_end];

    let contents_pattern = b"/Contents";
    let content_ref: Option<i32> =
        if let Some(contents_pos) = find_bytes_pattern_from(page_section, contents_pattern, 0) {
            // Parse the content reference number after /Contents
            let after_contents = &page_section[contents_pos + 9..];
            extract_first_number(after_contents)
        } else {
            None
        };

    if let Some(content_obj_num) = content_ref {
        // Find and extract the content stream
        let obj_marker = format!("{} 0 obj", content_obj_num);
        let obj_marker_bytes = obj_marker.as_bytes();

        if let Some(obj_pos) = find_bytes_pattern_from(data, obj_marker_bytes, 0) {
            // Find the end of this object
            let search_area = &data[obj_pos..];
            let obj_end = find_bytes_pattern_from(search_area, b"endobj", 0).unwrap_or(5000);
            let obj_section = &data[obj_pos..obj_pos + obj_end.min(search_area.len())];

            // Check if stream is compressed
            let is_flate = find_bytes_pattern_from(obj_section, b"/FlateDecode", 0).is_some()
                || find_bytes_pattern_from(obj_section, b"/Filter", 0).is_some();

            // Look for stream data
            if let Some(stream_pos) = find_bytes_pattern_from(search_area, b"stream", 0) {
                let mut stream_data_start = obj_pos + stream_pos + 6; // After "stream"

                // Skip newline(s)
                while stream_data_start < data.len()
                    && (data[stream_data_start] == b'\r' || data[stream_data_start] == b'\n')
                {
                    stream_data_start += 1;
                }

                if let Some(endstream_pos) =
                    find_bytes_pattern_from(&data[stream_data_start..], b"endstream", 0)
                {
                    let mut actual_end = stream_data_start + endstream_pos;

                    // Remove trailing newlines before endstream
                    while actual_end > stream_data_start
                        && (data[actual_end - 1] == b'\r' || data[actual_end - 1] == b'\n')
                    {
                        actual_end -= 1;
                    }

                    let stream_data = &data[stream_data_start..actual_end];

                    // Decompress if needed
                    if is_flate && !stream_data.is_empty() {
                        use flate2::read::ZlibDecoder;
                        use std::io::Read;

                        let mut decoder = ZlibDecoder::new(stream_data);
                        let mut decompressed = Vec::new();
                        match decoder.read_to_end(&mut decompressed) {
                            Ok(_) => {
                                return Ok(decompressed);
                            }
                            Err(_) => {
                                // Decompression failed, fall through to return raw data
                            }
                        }
                    }

                    return Ok(stream_data.to_vec());
                }
            }
        }
    }

    // Return empty content if not found
    Ok(Vec::new())
}

/// Find bytes pattern starting from a position
fn find_bytes_pattern_from(haystack: &[u8], needle: &[u8], start: usize) -> Option<usize> {
    if needle.is_empty() || start >= haystack.len() || needle.len() > haystack.len() - start {
        return None;
    }
    haystack[start..]
        .windows(needle.len())
        .position(|window| window == needle)
        .map(|pos| start + pos)
}

/// Reverse find bytes pattern
fn rfind_bytes_pattern(haystack: &[u8], needle: &[u8]) -> Option<usize> {
    if needle.is_empty() || needle.len() > haystack.len() {
        return None;
    }
    haystack
        .windows(needle.len())
        .rposition(|window| window == needle)
}

/// Extract a number from the end of a byte slice (reading backwards)
fn extract_number_backwards(data: &[u8]) -> i32 {
    let mut digits = Vec::new();
    for &b in data.iter().rev() {
        if b.is_ascii_digit() {
            digits.push(b);
        } else if !digits.is_empty() {
            break;
        }
    }
    digits.reverse();
    String::from_utf8_lossy(&digits).parse().unwrap_or(0)
}

/// Extract first number from a byte slice
fn extract_first_number(data: &[u8]) -> Option<i32> {
    let mut start = 0;

    // Skip whitespace
    while start < data.len() && data[start].is_ascii_whitespace() {
        start += 1;
    }

    // Collect digits
    let mut end = start;
    while end < data.len() && data[end].is_ascii_digit() {
        end += 1;
    }

    if end > start {
        String::from_utf8_lossy(&data[start..end]).parse().ok()
    } else {
        None
    }
}

/// Find a byte pattern in a byte slice
fn find_bytes_pattern(haystack: &[u8], needle: &[u8]) -> Option<usize> {
    if needle.is_empty() || needle.len() > haystack.len() {
        return None;
    }
    haystack
        .windows(needle.len())
        .position(|window| window == needle)
}

/// Extract ExtGState definitions from an overlay PDF page
/// Returns a map of graphics state names (e.g., "GS40") to their dictionary definitions
fn extract_page_ext_g_states(data: &[u8], page_num: usize) -> HashMap<String, String> {
    let mut states = HashMap::new();

    // Find the page object
    let type_page_pattern = b"/Type /Page";
    let type_pages_pattern = b"/Type /Pages";
    let obj_pattern = b" 0 obj";

    let mut page_objects: Vec<(usize, i32)> = Vec::new();
    let mut search_pos = 0;

    while let Some(type_pos) = find_bytes_pattern_from(data, type_page_pattern, search_pos) {
        if data[type_pos..].starts_with(type_pages_pattern) {
            search_pos = type_pos + type_pages_pattern.len();
            continue;
        }

        if let Some(obj_pos) = rfind_bytes_pattern(&data[..type_pos], obj_pattern) {
            let before_obj = &data[..obj_pos];
            let obj_num = extract_number_backwards(before_obj);
            if obj_num > 0 {
                page_objects.push((type_pos, obj_num));
            }
        }

        search_pos = type_pos + type_page_pattern.len();
    }

    if page_num >= page_objects.len() {
        return states;
    }

    let (page_type_pos, _page_obj_num) = page_objects[page_num];

    // Find /Resources and /ExtGState within the page object
    // Look in a window around the page definition
    let search_start = page_type_pos.saturating_sub(500);
    let search_end = (page_type_pos + 2000).min(data.len());
    let page_section = &data[search_start..search_end];

    // Look for /ExtGState dictionary
    if let Some(ext_pos) = find_bytes_pattern_from(page_section, b"/ExtGState", 0) {
        // Find the dictionary that follows
        let after_ext = &page_section[ext_pos + 10..];

        // Skip whitespace to find <<
        let mut dict_start = 0;
        while dict_start < after_ext.len() && after_ext[dict_start].is_ascii_whitespace() {
            dict_start += 1;
        }

        if dict_start + 2 < after_ext.len() && &after_ext[dict_start..dict_start + 2] == b"<<" {
            // Find matching >>
            let mut depth = 1;
            let mut dict_end = dict_start + 2;
            while dict_end < after_ext.len() - 1 && depth > 0 {
                if &after_ext[dict_end..dict_end + 2] == b"<<" {
                    depth += 1;
                    dict_end += 2;
                } else if &after_ext[dict_end..dict_end + 2] == b">>" {
                    depth -= 1;
                    if depth > 0 {
                        dict_end += 2;
                    }
                } else {
                    dict_end += 1;
                }
            }

            if depth == 0 {
                let dict_content = &after_ext[dict_start + 2..dict_end];

                // Parse individual graphics state entries like /GS40<</Type/ExtGState/ca 0.4/CA 0.4>>
                let dict_str = String::from_utf8_lossy(dict_content);

                // Simple parser for /GSxx<<...>> entries
                let mut pos = 0;
                while pos < dict_str.len() {
                    if dict_str[pos..].starts_with("/GS") {
                        // Find the name end
                        let name_start = pos + 1; // Skip /
                        let mut name_end = name_start;
                        while name_end < dict_str.len() {
                            let c = dict_str.chars().nth(name_end).unwrap_or(' ');
                            if c.is_whitespace() || c == '<' || c == '/' {
                                break;
                            }
                            name_end += 1;
                        }
                        let name = &dict_str[name_start..name_end];

                        // Find the dictionary for this entry
                        // Skip whitespace to find <<
                        let mut inner_start = name_end;
                        while inner_start < dict_str.len() {
                            let c = dict_str.chars().nth(inner_start).unwrap_or(' ');
                            if !c.is_whitespace() {
                                break;
                            }
                            inner_start += 1;
                        }

                        if inner_start + 2 <= dict_str.len()
                            && &dict_str[inner_start..inner_start + 2] == "<<"
                        {
                            // Find matching >>
                            let mut inner_depth = 1;
                            let mut inner_end = inner_start + 2;
                            while inner_end < dict_str.len() - 1 && inner_depth > 0 {
                                if &dict_str[inner_end..inner_end + 2] == "<<" {
                                    inner_depth += 1;
                                    inner_end += 2;
                                } else if &dict_str[inner_end..inner_end + 2] == ">>" {
                                    inner_depth -= 1;
                                    if inner_depth > 0 {
                                        inner_end += 2;
                                    }
                                } else {
                                    inner_end += 1;
                                }
                            }

                            if inner_depth == 0 {
                                let inner_dict = &dict_str[inner_start..inner_end + 2];
                                states.insert(name.to_string(), inner_dict.to_string());
                            }
                        }

                        pos = name_end;
                    } else {
                        pos += 1;
                    }
                }
            }
        }
    }

    states
}

/// Add ExtGState definitions to a page's resources
fn add_ext_g_states_to_page(
    target_data: &mut Vec<u8>,
    page_obj_num: i32,
    ext_g_states: &HashMap<String, String>,
) {
    if ext_g_states.is_empty() {
        return;
    }

    // Find the page object
    let obj_marker = format!("{} 0 obj", page_obj_num);
    let obj_marker_bytes = obj_marker.as_bytes();

    let Some(obj_pos) = find_bytes_pattern_from(target_data, obj_marker_bytes, 0) else {
        return;
    };

    // Find /Resources in the page object
    let page_section_end = (obj_pos + 3000).min(target_data.len());
    let page_section = &target_data[obj_pos..page_section_end];

    let resources_pattern = b"/Resources";
    let Some(resources_rel_pos) = find_bytes_pattern_from(page_section, resources_pattern, 0)
    else {
        // No /Resources found - need to add one
        // Find the end of the page dictionary (before >>)
        if let Some(dict_end) = rfind_bytes_pattern(page_section, b">>") {
            let insert_pos = obj_pos + dict_end;

            // Build ExtGState dictionary
            let mut ext_dict = String::from("/Resources<</ExtGState<<");
            for (name, def) in ext_g_states {
                ext_dict.push('/');
                ext_dict.push_str(name);
                ext_dict.push_str(def);
            }
            ext_dict.push_str(">>>>");

            target_data.splice(insert_pos..insert_pos, ext_dict.as_bytes().iter().copied());
        }
        return;
    };

    let resources_abs_pos = obj_pos + resources_rel_pos;

    // Check if /ExtGState already exists in Resources
    let after_resources = &target_data[resources_abs_pos + 10..page_section_end];

    // Skip whitespace to find << or reference
    let mut dict_start = 0;
    while dict_start < after_resources.len() && after_resources[dict_start].is_ascii_whitespace() {
        dict_start += 1;
    }

    // Check if resources is inline (<<) or a reference (N 0 R)
    if dict_start + 2 <= after_resources.len()
        && &after_resources[dict_start..dict_start + 2] == b"<<"
    {
        // Inline resources dictionary
        add_ext_g_states_to_inline_resources(
            target_data,
            resources_abs_pos,
            ext_g_states,
            page_section_end,
        );
    } else if after_resources[dict_start].is_ascii_digit() {
        // Indirect reference: /Resources N 0 R
        if let Some(ref_obj_num) = extract_first_number(&after_resources[dict_start..]) {
            add_ext_g_states_to_referenced_resources(target_data, ref_obj_num, ext_g_states);
        }
    }
}

/// Add ExtGState entries to an inline resources dictionary
fn add_ext_g_states_to_inline_resources(
    target_data: &mut Vec<u8>,
    resources_abs_pos: usize,
    ext_g_states: &HashMap<String, String>,
    page_section_end: usize,
) {
    let after_resources = &target_data[resources_abs_pos + 10..page_section_end];

    // Skip whitespace to find <<
    let mut dict_start = 0;
    while dict_start < after_resources.len() && after_resources[dict_start].is_ascii_whitespace() {
        dict_start += 1;
    }

    if dict_start + 2 > after_resources.len()
        || &after_resources[dict_start..dict_start + 2] != b"<<"
    {
        return;
    }

    // Check for existing /ExtGState
    if let Some(ext_gs_pos) = find_bytes_pattern_from(after_resources, b"/ExtGState", 0) {
        // ExtGState already exists - merge into it
        let ext_gs_abs_pos = resources_abs_pos + 10 + ext_gs_pos;
        merge_ext_g_state_entries(target_data, ext_gs_abs_pos, ext_g_states);
    } else {
        // No ExtGState - add one after /Resources<<
        let insert_pos = resources_abs_pos + 10 + dict_start + 2;
        let mut ext_dict = String::from("/ExtGState<<");
        for (name, def) in ext_g_states {
            ext_dict.push('/');
            ext_dict.push_str(name);
            ext_dict.push_str(def);
        }
        ext_dict.push_str(">>");
        target_data.splice(insert_pos..insert_pos, ext_dict.as_bytes().iter().copied());
    }
}

/// Add ExtGState entries to a referenced (indirect) resources object
fn add_ext_g_states_to_referenced_resources(
    target_data: &mut Vec<u8>,
    resources_obj_num: i32,
    ext_g_states: &HashMap<String, String>,
) {
    // Find the resources object in the PDF
    let obj_marker = format!("{} 0 obj", resources_obj_num);
    let obj_marker_bytes = obj_marker.as_bytes();

    let Some(obj_pos) = find_bytes_pattern_from(target_data, obj_marker_bytes, 0) else {
        return;
    };

    // Find the object's dictionary start
    let obj_section_end = (obj_pos + 5000).min(target_data.len());
    let obj_section = &target_data[obj_pos..obj_section_end];

    // Find << after "N 0 obj"
    let after_obj = &obj_section[obj_marker_bytes.len()..];
    let mut dict_start_rel = 0;
    while dict_start_rel < after_obj.len() && after_obj[dict_start_rel].is_ascii_whitespace() {
        dict_start_rel += 1;
    }

    if dict_start_rel + 2 > after_obj.len()
        || &after_obj[dict_start_rel..dict_start_rel + 2] != b"<<"
    {
        return;
    }

    let dict_abs_start = obj_pos + obj_marker_bytes.len() + dict_start_rel;

    // Check for existing /ExtGState within this object
    let search_end = (dict_abs_start + 4000).min(target_data.len());
    let dict_section = &target_data[dict_abs_start..search_end];

    if let Some(ext_gs_rel) = find_bytes_pattern_from(dict_section, b"/ExtGState", 0) {
        // ExtGState already exists - merge entries
        let ext_gs_abs = dict_abs_start + ext_gs_rel;
        merge_ext_g_state_entries(target_data, ext_gs_abs, ext_g_states);
    } else {
        // No ExtGState in resources object - add one after <<
        let insert_pos = dict_abs_start + 2;
        let mut ext_dict = String::from("/ExtGState<<");
        for (name, def) in ext_g_states {
            ext_dict.push('/');
            ext_dict.push_str(name);
            ext_dict.push_str(def);
        }
        ext_dict.push_str(">>");
        target_data.splice(insert_pos..insert_pos, ext_dict.as_bytes().iter().copied());
    }
}

/// Merge ExtGState entries into an existing /ExtGState dictionary
fn merge_ext_g_state_entries(
    target_data: &mut Vec<u8>,
    ext_gs_abs_pos: usize,
    ext_g_states: &HashMap<String, String>,
) {
    let after_ext_gs = &target_data[ext_gs_abs_pos + 10..];

    // Find the dictionary
    let mut ext_dict_start = 0;
    while ext_dict_start < after_ext_gs.len() && after_ext_gs[ext_dict_start].is_ascii_whitespace()
    {
        ext_dict_start += 1;
    }

    if ext_dict_start + 2 > after_ext_gs.len()
        || &after_ext_gs[ext_dict_start..ext_dict_start + 2] != b"<<"
    {
        return;
    }

    // Find the end of the existing ExtGState dict
    let mut depth = 1;
    let mut dict_end = ext_dict_start + 2;
    while dict_end < after_ext_gs.len() - 1 && depth > 0 {
        if &after_ext_gs[dict_end..dict_end + 2] == b"<<" {
            depth += 1;
            dict_end += 2;
        } else if &after_ext_gs[dict_end..dict_end + 2] == b">>" {
            depth -= 1;
            if depth > 0 {
                dict_end += 2;
            }
        } else {
            dict_end += 1;
        }
    }

    if depth == 0 {
        // Insert new entries before the closing >>
        let insert_pos = ext_gs_abs_pos + 10 + dict_end;
        let mut new_entries = String::new();
        for (name, def) in ext_g_states {
            new_entries.push('/');
            new_entries.push_str(name);
            new_entries.push_str(def);
        }
        target_data.splice(
            insert_pos..insert_pos,
            new_entries.as_bytes().iter().copied(),
        );
    }
}
