mod utils;
#[cfg(feature = "svg")]
mod svg;
mod table;
mod text;
mod media;
mod links;
mod vector;
mod resources;
mod destinations;
pub use utils::*;

use crate::color::color_to_pdf_rgb;
use crate::font_subset::FontEmbedding;
use crate::pdf_writer::PdfWriter;
use crate::{
    error::Result,
    font::FontRegistry,
    layout::{LayoutDocument, LayoutItem},
    model::{Attachment, NamedDest, OutlineEntry, RenderOptions},
};
use links::LinkAnnotation;
use std::collections::HashMap;
use std::io::Write;

pub struct RenderConfig<'a> {
    pub pdf_version: Option<&'a str>,
    pub compress: bool,
    pub language: Option<&'a str>,
    pub tagged: bool,
    pub named_dests: &'a [NamedDest],
    pub outlines: &'a [OutlineEntry],
    pub attachments: &'a [Attachment],
}

enum RenderObject {
    Plain(Vec<u8>),
    Stream { dictionary: Vec<u8>, data: Vec<u8> },
}

#[derive(Debug, Clone)]
pub(crate) struct ImageResource {
    pub(crate) name: String,
    pub(crate) object_id: usize,
    pub(crate) image: crate::layout::LayoutImage,
}

pub fn render_pdf(document: &LayoutDocument, fonts: &FontRegistry, options: &RenderOptions, info: Option<&crate::model::DocumentInfo>, encryption: Option<&crate::model::PdfEncryption>, config: &RenderConfig) -> Result<Vec<u8>> {
    let used_fonts = resources::collect_used_fonts(document, fonts)?;

    let mut font_embeddings: HashMap<String, FontEmbedding> = HashMap::new();
    for (family, glyphs) in &used_fonts {
        let loaded = fonts.get(family).unwrap();
        let embedding = FontEmbedding::prepare(family, &loaded.subset_data, glyphs)?;
        font_embeddings.insert(family.clone(), embedding);
    }

    let mut glyph_cache: HashMap<String, HashMap<char, u16>> = HashMap::new();
    for (family, embedding) in &font_embeddings {
        let mut map = HashMap::new();
        for glyph in &embedding.glyphs {
            map.insert(glyph.unicode, glyph.embedded_cid);
        }
        glyph_cache.insert(family.clone(), map);
    }

    let page_count = document.pages.len();

    // Determine effective compression level
    let effective_compression = if config.compress { options.compression_level } else { crate::model::CompressionLevel::None };

    let mut font_resource_entries = vec![
        "/F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_string(),
        "/F2 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>".to_string(),
        "/F3 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Oblique >>".to_string(),
        "/F4 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica-BoldOblique >>".to_string(),
    ];

    let mut font_objects = Vec::new();
    let mut font_obj_index = 3;
    let mut custom_font_map: HashMap<String, String> = HashMap::new();
    let mut custom_font_resource_index = 5;

    for (family, embedding) in &font_embeddings {
        let loaded = fonts.get(family).unwrap();
        let resource_name = family.replace(' ', "");
        let font_res_name = format!("/F{}", custom_font_resource_index);
        custom_font_map.insert(family.clone(), font_res_name.clone());
        font_resource_entries.push(format!("{} {} 0 R", font_res_name, font_obj_index));
        let descendant_obj = font_obj_index + 1;
        let descriptor_obj = font_obj_index + 2;
        let font_file_obj = font_obj_index + 3;
        let to_unicode_obj = font_obj_index + 4;

        let cmap = crate::font::build_to_unicode_cmap_from_glyphs(family, &embedding.glyphs);
        let (to_unicode_dict, compressed_cmap) = crate::pdf_stream::flate_stream_dictionary(cmap.as_bytes(), effective_compression)?;
        let to_unicode_stream = RenderObject::Stream {
            dictionary: to_unicode_dict,
            data: compressed_cmap,
        };

        let (font_file_dict, compressed_font) = crate::pdf_stream::flate_stream_dictionary(&embedding.font_data, effective_compression)?;
        let font_file_stream = RenderObject::Stream {
            dictionary: font_file_dict,
            data: compressed_font,
        };

        let bbox_min = loaded.metrics.descender.min(loaded.metrics.ascender);
        let bbox_max = loaded.metrics.descender.max(loaded.metrics.ascender);

        font_objects.push(RenderObject::Plain(
            format!(
                "<< /Type /Font /Subtype /Type0 /BaseFont /{0} /Encoding /Identity-H /DescendantFonts [{1} 0 R] /ToUnicode {2} 0 R >>",
                resource_name, descendant_obj, to_unicode_obj
            ).into_bytes()
        ));

        let width_array = format_width_array(&embedding.glyphs);
        font_objects.push(RenderObject::Plain(
            format!(
                "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /{0} /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor {1} 0 R /CIDToGIDMap /Identity {2} >>",
                resource_name, descriptor_obj, width_array
            ).into_bytes()
        ));

        font_objects.push(RenderObject::Plain(
            format!(
                "<< /Type /FontDescriptor /FontName /{0} /Flags 4 /Ascent {1} /Descent {2} /CapHeight {1} /ItalicAngle 0 /StemV 80 /FontBBox [{3} {4} {5} {6}] /FontFile2 {7} 0 R >>",
                resource_name,
                loaded.metrics.ascender,
                loaded.metrics.descender,
                bbox_min,
                bbox_min,
                bbox_max,
                bbox_max,
                font_file_obj
            ).into_bytes()
        ));

        font_objects.push(font_file_stream);
        font_objects.push(to_unicode_stream);

        font_obj_index += 5;
        custom_font_resource_index += 1;
    }

    let mut image_resources = Vec::new();
    let mut image_index = 1;
    for page in &document.pages {
        if let Some(ref bg_items) = page.background {
            resources::collect_page_image_resources(bg_items, &mut image_resources, &mut image_index, &mut font_obj_index);
        }
        if let Some(header) = &page.header {
            resources::collect_page_image_resources(&header.items, &mut image_resources, &mut image_index, &mut font_obj_index);
        }
        resources::collect_page_image_resources(&page.items, &mut image_resources, &mut image_index, &mut font_obj_index);
        if let Some(footer) = &page.footer {
            resources::collect_page_image_resources(&footer.items, &mut image_resources, &mut image_index, &mut font_obj_index);
        }
    }

    let opacity_values = resources::collect_opacity_values(document);
    let ext_g_state_count = opacity_values.len();
    let ext_g_state_base_id = font_obj_index + image_resources.len();
    let base_page_start = ext_g_state_base_id + ext_g_state_count;

    let page_links: Vec<Vec<LinkAnnotation>> = document.pages.iter().map(|page| {
        let ph = document.page_size.height;
        let mut page_links = Vec::new();
        if let Some(ref bg_items) = page.background {
            links::collect_links_from_items(bg_items, ph, &mut page_links);
        }
        if let Some(ref header) = page.header {
            links::collect_links_from_items(&header.items, ph, &mut page_links);
        }
        links::collect_links_from_items(&page.items, ph, &mut page_links);
        if let Some(ref footer) = page.footer {
            links::collect_links_from_items(&footer.items, ph, &mut page_links);
        }
        page_links
    }).collect();

    let page_start = base_page_start;
    let font_resources = font_resource_entries.join(" ");

    let mut writer = PdfWriter::new();
    
    if let Some(enc) = encryption {
        writer.setup_encryption(enc);
    }
    
    writer.add_object(b"<< /Type /Catalog /Pages 2 0 R >>".to_vec());
    writer.add_object(b"<< /Type /Pages /Kids [] /Count 0 >>".to_vec());
    for object in font_objects {
        match object {
            RenderObject::Plain(bytes) => { writer.add_object(bytes); },
            RenderObject::Stream { dictionary, data } => { writer.add_object_with_stream(dictionary, data); },
        }
    }

    for resource in &image_resources {
        let filter = match resource.image.format {
            crate::image::ImageFormat::Jpeg => "/DCTDecode",
            crate::image::ImageFormat::Png => "/FlateDecode",
        };
        let dictionary = format!(
            "<< /Type /XObject /Subtype /Image /Width {} /Height {} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter {} /Length {} >>",
            resource.image.intrinsic_width,
            resource.image.intrinsic_height,
            filter,
            resource.image.data.len()
        ).into_bytes();
        writer.add_object_with_stream(dictionary, resource.image.data.as_ref().clone());
    }

    for &pct in &opacity_values {
        writer.add_object(resources::make_ext_g_state(pct));
    }

    let mut page_obj_ids: Vec<usize> = Vec::with_capacity(page_count);
    let mut next_obj_id = page_start;

    if let Some(cb) = &options.progress_callback {
        cb(0.0);
    }

    for (index, page) in document.pages.iter().enumerate() {
        let page_obj = next_obj_id;
        let content_obj = next_obj_id + 1;
        page_obj_ids.push(page_obj);
        next_obj_id += 2 + page_links[index].len();

        let xobject_resources = resources::image_resources_for_page(page, &image_resources);

        let ext_g_state_resources = if opacity_values.is_empty() {
            String::new()
        } else {
            let entries: Vec<String> = opacity_values.iter().enumerate()
                .map(|(i, p)| format!("/gs{} {} 0 R", p, ext_g_state_base_id + i))
                .collect();
            format!("/ExtGState << {} >>", entries.join(" "))
        };

        let mut annots = String::new();
        if !page_links[index].is_empty() {
            let ann_start = content_obj + 1;
            let refs: Vec<String> = (0..page_links[index].len())
                .map(|ai| format!("{} 0 R", ann_start + ai))
                .collect();
            annots = format!(" /Annots [{}]", refs.join(" "));
        }

        let page_object = format!(
            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {:.2} {:.2}] /Resources << /Font << {} >> {} {} >> /Contents {} 0 R{} >>",
            document.page_size.width,
            document.page_size.height,
            font_resources,
            xobject_resources,
            ext_g_state_resources,
            content_obj,
            annots
        );
        writer.add_object(page_object.into_bytes());
        let stream = render_page_stream(
            document.page_size.height,
            document.page_size.width,
            page,
            &glyph_cache,
            &custom_font_map,
            &image_resources,
        );
        let (dictionary, compressed) = crate::pdf_stream::flate_stream_dictionary(&stream, effective_compression)?;
        writer.add_object_with_stream(dictionary, compressed);

        for link in &page_links[index] {
            if let Some(ref dest) = link.destination {
                writer.add_object(format!(
                    "<< /Type /Annot /Subtype /Link /Border [0 0 0] /Rect [{:.2} {:.2} {:.2} {:.2}] /Dest ({}) >>",
                    link.x, link.y, link.x + link.width, link.y + link.height, dest
                ).into_bytes());
            } else {
                let escaped = escape_pdf_uri(&link.url);
                writer.add_object(format!(
                    "<< /Type /Annot /Subtype /Link /Border [0 0 0] /Rect [{:.2} {:.2} {:.2} {:.2}] /A << /Type /Action /S /URI /URI ({}) >> >>",
                    link.x, link.y, link.x + link.width, link.y + link.height, escaped
                ).into_bytes());
            }
        }

        if let Some(cb) = &options.progress_callback {
            cb((index + 1) as f32 / page_count as f32);
        }
    }

    let kids = page_obj_ids
        .iter()
        .map(|id| format!("{} 0 R", id))
        .collect::<Vec<_>>()
        .join(" ");
    let pages_obj = format!("<< /Type /Pages /Kids [{}] /Count {} >>", kids, page_count);
    writer.update_object(2, pages_obj.into_bytes());

    // Build Catalog with all features
    let mut catalog_entries = vec!["/Type /Catalog".to_string(), "/Pages 2 0 R".to_string()];

    if let Some(lang) = config.language {
        catalog_entries.push(format!("/Lang ({})", lang));
    }

    if config.tagged {
        catalog_entries.push("/MarkInfo << /Marked true >>".to_string());
    }

    // Create attachment objects (EmbeddedFile streams + Filespec dicts)
    if !config.attachments.is_empty() {
        let mut names_entries = Vec::new();
        for att in config.attachments {
            let ef_obj_id = writer.next_object_id();
            let ef_dict = format!("<< /Type /EmbeddedFile /Length {} >>", att.data.len());
            writer.add_object_with_stream(ef_dict.into_bytes(), att.data.clone());

            let fs_obj_id = writer.next_object_id();
            let fs_dict = format!(
                "<< /Type /Filespec /F ({}) /EF << /F {} 0 R >> >>",
                att.name.replace('\\', "\\\\").replace('(', "\\(").replace(')', "\\)"),
                ef_obj_id
            );
            writer.add_object(fs_dict.into_bytes());
            names_entries.push(format!("({}) {} 0 R", att.name, fs_obj_id));
        }
        catalog_entries.push(format!("/Names << /EmbeddedFiles << /Names [{}] >> >>", names_entries.join(" ")));
    }

    // Create outline objects if outlines exist
    if !config.outlines.is_empty() {
        let outlines_obj_id = writer.add_object(b"<< >>".to_vec()); // placeholder for root outline
        let mut outline_obj_ids = Vec::new();
        for entry in config.outlines {
            let obj_id = writer.add_object(b"<< >>".to_vec()); // placeholder for each entry
            outline_obj_ids.push((obj_id, entry));
        }

        // Update each outline item with actual content
        for (i, (obj_id, entry)) in outline_obj_ids.iter().enumerate() {
            let prev_ref = if i > 0 { format!("/Prev {} 0 R", outline_obj_ids[i-1].0) } else { String::new() };
            let next_ref = if i < outline_obj_ids.len() - 1 { format!("/Next {} 0 R", outline_obj_ids[i+1].0) } else { String::new() };
            let parent_ref = if let Some(parent_id) = &entry.parent_id {
                config.outlines.iter().zip(outline_obj_ids.iter())
                    .find(|(_oe, (_, pe))| pe.id == *parent_id && pe.parent_id.is_none())
                    .map(|(_, (pid, _))| format!("/Parent {} 0 R", pid))
                    .unwrap_or_else(|| format!("/Parent {} 0 R", outlines_obj_id))
            } else {
                format!("/Parent {} 0 R", outlines_obj_id)
            };

            // Resolve destination from named_dests
            let dest = if let Some(nd) = config.named_dests.iter().find(|n| n.id == entry.id) {
                let page_idx = nd.page_index.min(page_obj_ids.len().saturating_sub(1));
                format!(" /Dest [{} 0 R /XYZ {:.2} {:.2} 0]", page_obj_ids[page_idx], nd.x, nd.y)
            } else {
                String::new()
            };

            let title_str = if entry.text.is_ascii() {
                format!("({})", entry.text.replace('\\', "\\\\").replace('(', "\\(").replace(')', "\\)"))
            } else {
                let mut hex = String::with_capacity(4 + entry.text.len() * 4);
                hex.push_str("FEFF");
                for ch in entry.text.encode_utf16() {
                    hex.push_str(&format!("{:04X}", ch));
                }
                format!("<{}>", hex)
            };
            let dict = format!(
                "<< /Type /Outline /Title {} {} {} {}{} /Count 0 >>",
                title_str, parent_ref, prev_ref, next_ref, dest
            );
            writer.update_object(*obj_id, dict.into_bytes());
        }

        // Create root outline object
        let first_ref = if !outline_obj_ids.is_empty() { format!("/First {} 0 R", outline_obj_ids[0].0) } else { String::new() };
        let last_ref = if !outline_obj_ids.is_empty() { format!("/Last {} 0 R", outline_obj_ids.last().unwrap().0) } else { String::new() };
        let root_dict = format!(
            "<< /Type /Outline /Count {} {} {} >>",
            outline_obj_ids.len(), first_ref, last_ref
        );
        writer.update_object(outlines_obj_id, root_dict.into_bytes());
        catalog_entries.push(format!("/Outlines {} 0 R", outlines_obj_id));
    }

    // Create named destinations (not already covered by outlines)
    let outline_ids: Vec<&str> = config.outlines.iter().map(|e| e.id.as_str()).collect();
    let remaining_dests: Vec<_> = config.named_dests.iter()
        .filter(|nd| !outline_ids.contains(&nd.id.as_str()))
        .collect();
    
    // Collect named destinations from layout items with id
    let mut auto_dests: Vec<crate::model::NamedDest> = Vec::new();
    for (page_idx, page) in document.pages.iter().enumerate() {
        destinations::collect_named_dests_from_items(&page.items, page_idx, &mut auto_dests);
    }
    
    if !remaining_dests.is_empty() || !auto_dests.is_empty() {
        let mut dest_entries = Vec::new();
        for nd in &remaining_dests {
            let page_idx = nd.page_index.min(page_obj_ids.len().saturating_sub(1));
            dest_entries.push(format!("({}) [{} 0 R /XYZ {:.2} {:.2} 0]",
                nd.id, page_obj_ids[page_idx], nd.x, nd.y));
        }
        for nd in &auto_dests {
            let page_idx = nd.page_index.min(page_obj_ids.len().saturating_sub(1));
            dest_entries.push(format!("({}) [{} 0 R /XYZ {:.2} {:.2} 0]",
                nd.id, page_obj_ids[page_idx], nd.x, nd.y));
        }
        catalog_entries.push(format!("/Names << /Dests << /Names [{}] >> >>", dest_entries.join(" ")));
    }

    let catalog = format!("<< {} >>", catalog_entries.join(" "));
    writer.update_object(1, catalog.into_bytes());

    if let Some(cb) = &options.progress_callback {
        cb(1.0);
    }

    Ok(writer.finish(1, info, encryption, config.pdf_version))
}

fn render_watermark(
    page_width: f32,
    page_height: f32,
    wm: &crate::layout::LayoutWatermark,
    glyph_cache: &HashMap<String, HashMap<char, u16>>,
    custom_font_map: &HashMap<String, String>,
) -> Vec<u8> {
    let cx = page_width / 2.0;
    let cy = page_height / 2.0;
    let angle_rad = -wm.angle * std::f32::consts::PI / 180.0;
    let cos_a = angle_rad.cos();
    let sin_a = angle_rad.sin();

    let mut buf = Vec::with_capacity(256);
    buf.extend_from_slice(b"q\n");

    let opacity_pct = (wm.opacity * 100.0).round().clamp(0.0, 100.0) as u8;
    if opacity_pct < 100 {
        write_pdf!(buf, "/gs{} gs\n", opacity_pct);
    }

    write_pdf!(buf, "{:.4} {:.4} {:.4} {:.4} {:.2} {:.2} cm\n", cos_a, sin_a, -sin_a, cos_a, cx, cy);
    write_pdf!(buf, "{:.3} {:.3} {:.3} rg\n", wm.color_r, wm.color_g, wm.color_b);

    // Determine font: use custom font if available, otherwise Helvetica
    let is_custom = wm.font_family.as_ref()
        .and_then(|f| glyph_cache.get(f.as_str()))
        .is_some();

    // Vertical offset: PDF Td positions baseline, JS positions top-left corner.
    // JS uses cy - text_height/2 for vertical centering. Approximate text_height as 0.8 * font_size.
    let y_offset = -wm.font_size * 0.4;

    if is_custom {
        let family = wm.font_family.as_ref().unwrap();
        let pdf_font_name = custom_font_map.get(family.as_str())
            .cloned()
            .unwrap_or_else(|| family.replace(' ', ""));
        let cache = glyph_cache.get(family.as_str()).unwrap();
        buf.extend_from_slice(b"BT ");
        buf.extend_from_slice(pdf_font_name.as_bytes());
        write_pdf!(buf, " {:.1} Tf {:.2} {:.2} Td <", wm.font_size, -wm.text_width / 2.0, y_offset);
        const HEX: &[u8; 16] = b"0123456789ABCDEF";
        for ch in wm.text.chars() {
            if let Some(gid) = cache.get(&ch) {
                let v = *gid as u32;
                buf.push(HEX[((v >> 12) & 0xF) as usize]);
                buf.push(HEX[((v >> 8) & 0xF) as usize]);
                buf.push(HEX[((v >> 4) & 0xF) as usize]);
                buf.push(HEX[(v & 0xF) as usize]);
            }
        }
        buf.extend_from_slice(b"> Tj ET\n");
    } else {
        buf.extend_from_slice(b"BT\n");
        write_pdf!(buf, "/F1 {:.1} Tf\n", wm.font_size);
        write_pdf!(buf, "{:.2} {:.2} Td\n", -wm.text_width / 2.0, y_offset);
        buf.extend_from_slice(b"(");
        escape_pdf_text_to(&mut buf, &wm.text);
        buf.extend_from_slice(b") Tj\n");
        buf.extend_from_slice(b"ET\n");
    }

    buf.extend_from_slice(b"Q\n");
    buf
}

fn render_page_stream(
    page_height: f32,
    page_width: f32,
    page: &crate::layout::LayoutPage,
    glyph_cache: &HashMap<String, HashMap<char, u16>>,
    custom_font_map: &HashMap<String, String>,
    image_resources: &[ImageResource],
) -> Vec<u8> {
    let mut stream = Vec::with_capacity(4096);
    if let Some(ref bg_items) = page.background {
        for item in bg_items {
            stream.extend_from_slice(&render_page_stream_item(page_height, item, glyph_cache, custom_font_map, image_resources));
        }
    }
    if let Some(wm) = &page.watermark {
        stream.extend_from_slice(&render_watermark(page_width, page_height, wm, glyph_cache, custom_font_map));
    }
    if let Some(header) = &page.header {
        for item in &header.items {
            stream.extend_from_slice(&render_page_stream_item(page_height, item, glyph_cache, custom_font_map, image_resources));
        }
    }
    for item in &page.items {
        stream.extend_from_slice(&render_page_stream_item(page_height, item, glyph_cache, custom_font_map, image_resources));
    }
    if let Some(footer) = &page.footer {
        for item in &footer.items {
            stream.extend_from_slice(&render_page_stream_item(page_height, item, glyph_cache, custom_font_map, image_resources));
        }
    }
    stream
}

fn render_page_stream_item(
    page_height: f32,
    item: &LayoutItem,
    glyph_cache: &HashMap<String, HashMap<char, u16>>,
    custom_font_map: &HashMap<String, String>,
    image_resources: &[ImageResource],
) -> Vec<u8> {
    match item {
        LayoutItem::Text(text) => text::render_text_item(page_height, text, glyph_cache, custom_font_map),
        LayoutItem::Table(table) => table::render_table_item(page_height, table, glyph_cache, custom_font_map, image_resources),
        LayoutItem::Image(image) => media::render_image_item(page_height, image, image_resources),
        LayoutItem::Qr(qr) => media::render_qr_item(page_height, qr),
        #[cfg(feature = "svg")]
        LayoutItem::Svg(svg) => svg::render_svg_item(page_height, svg),
        #[cfg(not(feature = "svg"))]
        LayoutItem::Svg(_) => Vec::new(),
        LayoutItem::Columns(cols) => {
            let mut stream = Vec::new();
            for col in &cols.columns {
                for nested_item in col {
                    stream.extend_from_slice(&render_page_stream_item(page_height, nested_item, glyph_cache, custom_font_map, image_resources));
                }
            }
            stream
        }
        LayoutItem::List(list) => {
            let mut stream = Vec::new();
            for list_item in &list.items {
                // Draw marker
                match &list_item.marker {
                    Some(crate::layout::LayoutListMarker::Bullet { x, y, size, shape, color }) => {
                        let cy = page_height - y;
                        if let Some(c) = color {
                            if let Some((r, g, b)) = color_to_pdf_rgb(c) {
                                write_f32(&mut stream, r, 3); stream.push(b' ');
                                write_f32(&mut stream, g, 3); stream.push(b' ');
                                write_f32(&mut stream, b, 3); stream.extend_from_slice(b" rg\n");
                            }
                        }
                        match shape {
                            crate::layout::BulletShape::Disc => {
                                stream.extend_from_slice(&vector::render_circle(*x, cy, *size));
                            }
                            crate::layout::BulletShape::Circle => {
                                stream.extend_from_slice(&vector::render_circle_stroke(*x, cy, *size));
                            }
                            crate::layout::BulletShape::Square => {
                                stream.extend_from_slice(&vector::render_square_marker(*x, cy, *size));
                            }
                        }
                        if color.is_some() {
                            stream.extend_from_slice(b"0 g\n");
                        }
                    }
                    Some(crate::layout::LayoutListMarker::Number { text, x, y, font_size, font_family: _, color }) => {
                        let py = page_height - y - font_size;
                        if let Some(c) = color {
                            if let Some((r, g, b)) = color_to_pdf_rgb(c) {
                                write_f32(&mut stream, r, 3); stream.push(b' ');
                                write_f32(&mut stream, g, 3); stream.push(b' ');
                                write_f32(&mut stream, b, 3); stream.extend_from_slice(b" rg\n");
                            }
                        }
                        write_pdf!(stream, "BT /F1 {:.2} Tf {:.2} {:.2} Td (", font_size, x, py);
                        escape_pdf_text_to(&mut stream, text);
                        stream.extend_from_slice(b") Tj ET\n");
                        if color.is_some() {
                            stream.extend_from_slice(b"0 g\n");
                        }
                    }
                    None => {}
                }
                // Draw content
                for content_item in &list_item.content {
                    stream.extend_from_slice(&render_page_stream_item(
                        page_height, content_item, glyph_cache, custom_font_map, image_resources,
                    ));
                }
            }
            stream
        }
        LayoutItem::Canvas(canvas) => {
            let mut stream = Vec::new();
            let cx = canvas.x;
            let cy = canvas.y;
            for shape in &canvas.shapes {
                match shape {
                    crate::model::CanvasShape::Rect { x, y, w, h, color, line_color, line_width, fill_opacity, stroke_opacity } => {
                        let abs_x = cx + x;
                        let abs_y = cy + y;
                        let py = page_height - abs_y - h;
                        let ops = vector::build_rect_ops(abs_x, py, *w, *h, color.as_deref(), line_color.as_deref(), *line_width, *fill_opacity, *stroke_opacity);
                        stream.extend_from_slice(&ops);
                    }
                    crate::model::CanvasShape::Ellipse { x, y, r1, r2, color, line_color, line_width } => {
                        let abs_x = cx + x;
                        let abs_y = cy + y;
                        let cy_pdf = page_height - abs_y - r2;
                        let ops = vector::build_ellipse_ops(abs_x, cy_pdf, *r1, *r2, color.as_deref(), line_color.as_deref(), *line_width);
                        stream.extend_from_slice(&ops);
                    }
                    crate::model::CanvasShape::Line { x1, y1, x2, y2, line_color, line_width } => {
                        let abs_x1 = cx + x1;
                        let abs_y1 = cy + y1;
                        let abs_x2 = cx + x2;
                        let abs_y2 = cy + y2;
                        let py1 = page_height - abs_y1;
                        let py2 = page_height - abs_y2;
                        let ops = vector::build_line_ops(abs_x1, py1, abs_x2, py2, line_color.as_deref(), *line_width);
                        stream.extend_from_slice(&ops);
                    }
                    crate::model::CanvasShape::Polyline { points, close_path, color, line_color, line_width } => {
                        let ops = vector::build_polyline_ops_offset(points, *close_path, color.as_deref(), line_color.as_deref(), *line_width, page_height, cx, cy);
                        stream.extend_from_slice(&ops);
                    }
                }
            }
            stream
        }
        LayoutItem::Toc(toc) => {
            let mut stream = Vec::new();
            // Render title items
            for item in &toc.title {
                stream.extend_from_slice(&render_page_stream_item(page_height, item, glyph_cache, custom_font_map, image_resources));
            }
            // Render entries
            for entry in &toc.entries {
                let py = page_height - entry.y - entry.line_height;
                let font_size = entry.style.font_size;

                // Resolve font
                let (font_name, is_cid) = if let Some(family) = &entry.style.font_family {
                    let name = custom_font_map.get(family).cloned().unwrap_or_else(|| "F1".to_string());
                    let is_cid = glyph_cache.contains_key(family.as_str());
                    (name, is_cid)
                } else {
                    ("F1".to_string(), false)
                };

                // Render entry text
                if is_cid {
                    // CID font: use hex glyph IDs
                    let family = entry.style.font_family.as_deref().unwrap_or("");
                    if let Some(cache) = glyph_cache.get(family) {
                        stream.extend_from_slice(b"BT ");
                        stream.extend_from_slice(font_name.as_bytes());
                        write_pdf!(stream, " {:.2} Tf {:.2} {:.2} Td <", font_size, toc.x, py);
                        const HEX: &[u8; 16] = b"0123456789ABCDEF";
                        for ch in entry.text.chars() {
                            if let Some(gid) = cache.get(&ch) {
                                let v = *gid as u32;
                                stream.push(HEX[((v >> 12) & 0xF) as usize]);
                                stream.push(HEX[((v >> 8) & 0xF) as usize]);
                                stream.push(HEX[((v >> 4) & 0xF) as usize]);
                                stream.push(HEX[(v & 0xF) as usize]);
                            }
                        }
                        stream.extend_from_slice(b"> Tj ET\n");
                    }
                } else {
                    // Standard font: use Tj with escaped text
                    write_pdf!(stream, "BT /{} {:.2} Tf {:.2} {:.2} Td (", font_name, font_size, toc.x, py);
                    escape_pdf_text_to(&mut stream, &entry.text);
                    stream.extend_from_slice(b") Tj ET\n");
                }

                // Render page number if available (always use F1/Helvetica for numbers)
                if let Some(page_num) = entry.page_number {
                    let page_text = format!("{}", page_num);
                    let page_x = toc.x + toc.width - 30.0;
                    write_pdf!(stream, "BT /F1 {:.2} Tf {:.2} {:.2} Td (", font_size, page_x, py);
                    escape_pdf_text_to(&mut stream, &page_text);
                    stream.extend_from_slice(b") Tj ET\n");
                }
            }
            stream
        }
    }
}

