//! FFI bindings for fz_document_writer (Document Writers)
//!
//! This module provides functions for writing documents in various formats
//! including PDF, SVG, text, Office formats, and image formats.

use super::ffi_safety::{cstr_to_str, cstr_to_string};
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::sync::{LazyLock, Mutex};

use crate::ffi::stext::Rect;
use crate::ffi::{Handle, HandleStore};

/// Global store for document writers
pub static WRITERS: LazyLock<HandleStore<DocumentWriter>> = LazyLock::new(HandleStore::new);

/// Global store for writer devices (borrowed devices from begin_page)
pub static WRITER_DEVICES: LazyLock<HandleStore<WriterDevice>> = LazyLock::new(HandleStore::new);

/// Document writer format types
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriterFormat {
    Pdf = 0,
    Svg = 1,
    Text = 2,
    Html = 3,
    Xhtml = 4,
    Odt = 5,
    Docx = 6,
    Ps = 7,
    Pcl = 8,
    Pclm = 9,
    Pwg = 10,
    Cbz = 11,
    Csv = 12,
    Pdfocr = 13,
    Png = 14,
    Jpeg = 15,
    Pam = 16,
    Pnm = 17,
    Pgm = 18,
    Ppm = 19,
    Pbm = 20,
    Pkm = 21,
}

impl WriterFormat {
    /// Parse format from string
    pub fn from_str(s: &str) -> Option<Self> {
        match s.to_lowercase().as_str() {
            "pdf" => Some(WriterFormat::Pdf),
            "svg" => Some(WriterFormat::Svg),
            "text" | "txt" => Some(WriterFormat::Text),
            "html" => Some(WriterFormat::Html),
            "xhtml" => Some(WriterFormat::Xhtml),
            "odt" => Some(WriterFormat::Odt),
            "docx" => Some(WriterFormat::Docx),
            "ps" | "postscript" => Some(WriterFormat::Ps),
            "pcl" => Some(WriterFormat::Pcl),
            "pclm" => Some(WriterFormat::Pclm),
            "pwg" => Some(WriterFormat::Pwg),
            "cbz" => Some(WriterFormat::Cbz),
            "csv" => Some(WriterFormat::Csv),
            "pdfocr" | "ocr" => Some(WriterFormat::Pdfocr),
            "png" => Some(WriterFormat::Png),
            "jpeg" | "jpg" => Some(WriterFormat::Jpeg),
            "pam" => Some(WriterFormat::Pam),
            "pnm" => Some(WriterFormat::Pnm),
            "pgm" => Some(WriterFormat::Pgm),
            "ppm" => Some(WriterFormat::Ppm),
            "pbm" => Some(WriterFormat::Pbm),
            "pkm" => Some(WriterFormat::Pkm),
            _ => None,
        }
    }

    /// Get file extension for format
    pub fn extension(&self) -> &'static str {
        match self {
            WriterFormat::Pdf => "pdf",
            WriterFormat::Svg => "svg",
            WriterFormat::Text => "txt",
            WriterFormat::Html => "html",
            WriterFormat::Xhtml => "xhtml",
            WriterFormat::Odt => "odt",
            WriterFormat::Docx => "docx",
            WriterFormat::Ps => "ps",
            WriterFormat::Pcl => "pcl",
            WriterFormat::Pclm => "pclm",
            WriterFormat::Pwg => "pwg",
            WriterFormat::Cbz => "cbz",
            WriterFormat::Csv => "csv",
            WriterFormat::Pdfocr => "pdf",
            WriterFormat::Png => "png",
            WriterFormat::Jpeg => "jpg",
            WriterFormat::Pam => "pam",
            WriterFormat::Pnm => "pnm",
            WriterFormat::Pgm => "pgm",
            WriterFormat::Ppm => "ppm",
            WriterFormat::Pbm => "pbm",
            WriterFormat::Pkm => "pkm",
        }
    }

    /// Check if format is multi-page
    pub fn is_multipage(&self) -> bool {
        matches!(
            self,
            WriterFormat::Pdf
                | WriterFormat::Svg
                | WriterFormat::Text
                | WriterFormat::Html
                | WriterFormat::Xhtml
                | WriterFormat::Odt
                | WriterFormat::Docx
                | WriterFormat::Ps
                | WriterFormat::Pcl
                | WriterFormat::Pclm
                | WriterFormat::Pwg
                | WriterFormat::Cbz
                | WriterFormat::Csv
                | WriterFormat::Pdfocr
        )
    }

    /// Check if format is image-based
    pub fn is_image(&self) -> bool {
        matches!(
            self,
            WriterFormat::Png
                | WriterFormat::Jpeg
                | WriterFormat::Pam
                | WriterFormat::Pnm
                | WriterFormat::Pgm
                | WriterFormat::Ppm
                | WriterFormat::Pbm
                | WriterFormat::Pkm
        )
    }
}

/// Writer state
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriterState {
    Created,
    PageInProgress,
    Closed,
}

/// Document writer options
#[derive(Debug, Clone, Default)]
pub struct WriterOptions {
    /// Raw options string
    pub raw: String,
    /// Parsed key-value pairs
    pub options: HashMap<String, String>,
    /// Resolution (for image output)
    pub resolution: Option<f32>,
    /// Compression level
    pub compression: Option<i32>,
    /// Quality (for JPEG)
    pub quality: Option<i32>,
    /// Color mode
    pub colorspace: Option<String>,
    /// Page size template
    pub page_size: Option<String>,
    /// Linearize PDF
    pub linearize: bool,
    /// Encrypt PDF
    pub encrypt: bool,
    /// OCR language
    pub ocr_language: Option<String>,
}

impl WriterOptions {
    /// Parse options from string
    pub fn parse(opts_str: &str) -> Self {
        let mut options = WriterOptions {
            raw: opts_str.to_string(),
            ..Default::default()
        };

        for part in opts_str.split(',') {
            let part = part.trim();
            if part.is_empty() {
                continue;
            }

            if let Some((key, value)) = part.split_once('=') {
                let key = key.trim().to_lowercase();
                let value = value.trim();
                options.options.insert(key.clone(), value.to_string());

                match key.as_str() {
                    "resolution" | "res" | "dpi" => {
                        options.resolution = value.parse().ok();
                    }
                    "compression" => {
                        options.compression = value.parse().ok();
                    }
                    "quality" => {
                        options.quality = value.parse().ok();
                    }
                    "colorspace" | "cs" => {
                        options.colorspace = Some(value.to_string());
                    }
                    "pagesize" | "page-size" => {
                        options.page_size = Some(value.to_string());
                    }
                    "language" | "lang" | "ocr-language" => {
                        options.ocr_language = Some(value.to_string());
                    }
                    _ => {}
                }
            } else {
                // Boolean flag
                match part.to_lowercase().as_str() {
                    "linearize" | "linear" => options.linearize = true,
                    "encrypt" | "encrypted" => options.encrypt = true,
                    _ => {
                        options.options.insert(part.to_string(), String::new());
                    }
                }
            }
        }

        options
    }

    /// Get option value
    pub fn get(&self, key: &str) -> Option<&String> {
        self.options.get(key)
    }

    /// Check if option exists
    pub fn has(&self, key: &str) -> bool {
        self.options.contains_key(key)
    }
}

/// Page data for writer
#[derive(Debug, Clone)]
pub struct WriterPage {
    pub index: i32,
    pub mediabox: Rect,
    pub content: Vec<u8>,
}

/// Document writer
pub struct DocumentWriter {
    pub format: WriterFormat,
    pub path: Option<String>,
    pub output: Option<Handle>, // Handle to output stream
    pub buffer: Option<Handle>, // Handle to buffer
    pub options: WriterOptions,
    pub state: WriterState,
    pub pages: Vec<WriterPage>,
    pub current_page: Option<WriterPage>,
    pub current_device: Option<Handle>,
    pub page_count: i32,
}

impl DocumentWriter {
    /// Create a new document writer
    pub fn new(format: WriterFormat, path: Option<String>, options: WriterOptions) -> Self {
        Self {
            format,
            path,
            output: None,
            buffer: None,
            options,
            state: WriterState::Created,
            pages: Vec::new(),
            current_page: None,
            current_device: None,
            page_count: 0,
        }
    }

    /// Create writer with output handle
    pub fn with_output(format: WriterFormat, output: Handle, options: WriterOptions) -> Self {
        let mut writer = Self::new(format, None, options);
        writer.output = Some(output);
        writer
    }

    /// Create writer with buffer handle
    pub fn with_buffer(format: WriterFormat, buffer: Handle, options: WriterOptions) -> Self {
        let mut writer = Self::new(format, None, options);
        writer.buffer = Some(buffer);
        writer
    }

    /// Begin a new page
    pub fn begin_page(&mut self, mediabox: Rect) -> bool {
        if self.state == WriterState::Closed {
            return false;
        }

        // End current page if one is in progress
        if self.state == WriterState::PageInProgress {
            self.end_page();
        }

        self.current_page = Some(WriterPage {
            index: self.page_count,
            mediabox,
            content: Vec::new(),
        });
        self.state = WriterState::PageInProgress;
        true
    }

    /// End current page
    pub fn end_page(&mut self) -> bool {
        if self.state != WriterState::PageInProgress {
            return false;
        }

        if let Some(page) = self.current_page.take() {
            self.pages.push(page);
            self.page_count += 1;
        }

        self.current_device = None;
        self.state = WriterState::Created;
        true
    }

    /// Close the writer
    pub fn close(&mut self) -> bool {
        if self.state == WriterState::Closed {
            return false;
        }

        // End current page if in progress
        if self.state == WriterState::PageInProgress {
            self.end_page();
        }

        // Write output based on format
        self.write_output();

        self.state = WriterState::Closed;
        true
    }

    /// Write output (format-specific)
    fn write_output(&self) {
        match self.format {
            WriterFormat::Pdf => self.write_pdf(),
            WriterFormat::Svg => self.write_svg(),
            WriterFormat::Text => self.write_text(),
            WriterFormat::Html | WriterFormat::Xhtml => self.write_html(),
            WriterFormat::Cbz => self.write_cbz(),
            WriterFormat::Png
            | WriterFormat::Jpeg
            | WriterFormat::Pam
            | WriterFormat::Pnm
            | WriterFormat::Pgm
            | WriterFormat::Ppm
            | WriterFormat::Pbm
            | WriterFormat::Pkm => self.write_image(),
            _ => {
                // Other formats use generic write
            }
        }
    }

    /// Write PDF output
    ///
    /// Generates a complete PDF 1.7 file.  Each page's content stream
    /// is FLATE-compressed (`/Filter /FlateDecode`) to reduce file size,
    /// matching what professional PDF producers emit.
    fn write_pdf(&self) {
        use flate2::Compression;
        use flate2::write::ZlibEncoder;
        use std::io::Write;

        let mut pdf_data = Vec::new();

        // PDF header
        pdf_data.extend_from_slice(b"%PDF-1.7\n");
        pdf_data.extend_from_slice(b"%\xE2\xE3\xCF\xD3\n");

        let mut obj_offsets = Vec::new();
        let mut obj_num = 1;

        // Catalog (object 1)
        obj_offsets.push(pdf_data.len());
        let catalog = format!(
            "{} 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n",
            obj_num
        );
        pdf_data.extend_from_slice(catalog.as_bytes());
        obj_num += 1;

        // Pages (object 2)
        // Each page takes two object numbers: one for the content stream
        // and one for the page dict.  Page dicts start at object 3 and
        // are assigned in pairs (content_stream, page_dict).
        obj_offsets.push(pdf_data.len());
        let page_refs: String = (0..self.pages.len())
            .map(|i| format!("{} 0 R", 3 + i * 2 + 1))
            .collect::<Vec<_>>()
            .join(" ");
        let pages = format!(
            "{} 0 obj\n<< /Type /Pages /Kids [ {} ] /Count {} >>\nendobj\n",
            obj_num,
            page_refs,
            self.pages.len()
        );
        pdf_data.extend_from_slice(pages.as_bytes());
        obj_num += 1;

        // Page objects -- each page gets a content stream object first,
        // then the page dictionary that references it.
        for page in &self.pages {
            let content_obj_num = obj_num;
            let page_obj_num = obj_num + 1;

            // --- Content stream (FLATE-compressed) ---
            obj_offsets.push(pdf_data.len());

            let raw_content = &page.content;
            // Compress content stream data with zlib/deflate.
            // Skip compression for very small streams where the zlib
            // framing overhead outweighs the savings.
            const FLATE_MIN_SIZE: usize = 128;
            let (stream_bytes, use_flate) = if raw_content.len() < FLATE_MIN_SIZE {
                (raw_content.clone(), false)
            } else {
                let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
                if encoder.write_all(raw_content).is_ok() {
                    if let Ok(compressed) = encoder.finish() {
                        if compressed.len() < raw_content.len() {
                            (compressed, true)
                        } else {
                            // Compression didn't help; store raw
                            (raw_content.clone(), false)
                        }
                    } else {
                        (raw_content.clone(), false)
                    }
                } else {
                    (raw_content.clone(), false)
                }
            };

            let filter_entry = if use_flate {
                " /Filter /FlateDecode"
            } else {
                ""
            };
            let stream_header = format!(
                "{} 0 obj\n<< /Length {}{} >>\nstream\n",
                content_obj_num,
                stream_bytes.len(),
                filter_entry,
            );
            pdf_data.extend_from_slice(stream_header.as_bytes());
            pdf_data.extend_from_slice(&stream_bytes);
            pdf_data.extend_from_slice(b"\nendstream\nendobj\n");
            obj_num += 1;

            // --- Page dictionary ---
            obj_offsets.push(pdf_data.len());
            let page_obj = format!(
                "{} 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [ {} {} {} {} ] /Contents {} 0 R >>\nendobj\n",
                page_obj_num,
                page.mediabox.x0,
                page.mediabox.y0,
                page.mediabox.x1,
                page.mediabox.y1,
                content_obj_num,
            );
            pdf_data.extend_from_slice(page_obj.as_bytes());
            obj_num += 1;
        }

        // Xref table
        let xref_offset = pdf_data.len();
        pdf_data.extend_from_slice(b"xref\n");
        pdf_data.extend_from_slice(format!("0 {}\n", obj_num).as_bytes());
        pdf_data.extend_from_slice(b"0000000000 65535 f \n");
        for offset in &obj_offsets {
            pdf_data.extend_from_slice(format!("{:010} 00000 n \n", offset).as_bytes());
        }

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

        // Write to file if path specified
        if let Some(ref path) = self.path {
            let _ = std::fs::write(path, &pdf_data);
        }
    }

    /// Write SVG output
    fn write_svg(&self) {
        for (i, page) in self.pages.iter().enumerate() {
            let width = page.mediabox.x1 - page.mediabox.x0;
            let height = page.mediabox.y1 - page.mediabox.y0;

            let svg = format!(
                r#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}" viewBox="{} {} {} {}">
  <!-- Page {} -->
</svg>
"#,
                width,
                height,
                page.mediabox.x0,
                page.mediabox.y0,
                width,
                height,
                i + 1
            );

            if let Some(ref path) = self.path {
                let page_path = if self.pages.len() > 1 {
                    format!("{}_{}.svg", path.trim_end_matches(".svg"), i + 1)
                } else {
                    path.clone()
                };
                let _ = std::fs::write(page_path, svg.as_bytes());
            }
        }
    }

    /// Write text output
    fn write_text(&self) {
        let mut text = String::new();
        for (i, _page) in self.pages.iter().enumerate() {
            if i > 0 {
                text.push_str("\n\n--- Page Break ---\n\n");
            }
            text.push_str(&format!("--- Page {} ---\n", i + 1));
            // Page content would be added here
        }

        if let Some(ref path) = self.path {
            let _ = std::fs::write(path, text.as_bytes());
        }
    }

    /// Write HTML output
    fn write_html(&self) {
        let doctype = if self.format == WriterFormat::Xhtml {
            r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
"#
        } else {
            "<!DOCTYPE html>\n"
        };

        let mut html = String::from(doctype);
        html.push_str(
            "<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>Document</title>\n</head>\n<body>\n",
        );

        for (i, _page) in self.pages.iter().enumerate() {
            html.push_str(&format!("<div class=\"page\" id=\"page-{}\">\n", i + 1));
            html.push_str("</div>\n");
        }

        html.push_str("</body>\n</html>\n");

        if let Some(ref path) = self.path {
            let _ = std::fs::write(path, html.as_bytes());
        }
    }

    /// Write CBZ (Comic Book ZIP) output
    ///
    /// CBZ is a ZIP archive containing one PNG image per page.
    /// We build the ZIP structure manually (local file headers +
    /// central directory + end of central directory) to avoid
    /// an external zip crate dependency.
    fn write_cbz(&self) {
        let Some(ref path) = self.path else { return };

        let mut archive = Vec::new();
        let mut central_dir = Vec::new();
        let mut offsets = Vec::new();

        for (i, page) in self.pages.iter().enumerate() {
            let width = (page.mediabox.x1 - page.mediabox.x0).max(1.0) as u32;
            let height = (page.mediabox.y1 - page.mediabox.y0).max(1.0) as u32;
            let png_data = Self::generate_png(width, height);
            let filename = format!("page_{:04}.png", i + 1);
            let name_bytes = filename.as_bytes();

            // CRC-32 of uncompressed data (store method, no compression)
            let crc = Self::crc32_compute(&png_data);
            let size = png_data.len() as u32;

            offsets.push(archive.len() as u32);

            // Local file header
            archive.extend_from_slice(&0x04034b50u32.to_le_bytes()); // signature
            archive.extend_from_slice(&20u16.to_le_bytes()); // version needed
            archive.extend_from_slice(&0u16.to_le_bytes()); // flags
            archive.extend_from_slice(&0u16.to_le_bytes()); // compression: store
            archive.extend_from_slice(&0u16.to_le_bytes()); // mod time
            archive.extend_from_slice(&0u16.to_le_bytes()); // mod date
            archive.extend_from_slice(&crc.to_le_bytes());
            archive.extend_from_slice(&size.to_le_bytes()); // compressed size
            archive.extend_from_slice(&size.to_le_bytes()); // uncompressed size
            archive.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
            archive.extend_from_slice(&0u16.to_le_bytes()); // extra field len
            archive.extend_from_slice(name_bytes);
            archive.extend_from_slice(&png_data);

            // Central directory entry
            central_dir.extend_from_slice(&0x02014b50u32.to_le_bytes());
            central_dir.extend_from_slice(&20u16.to_le_bytes()); // version made by
            central_dir.extend_from_slice(&20u16.to_le_bytes()); // version needed
            central_dir.extend_from_slice(&0u16.to_le_bytes()); // flags
            central_dir.extend_from_slice(&0u16.to_le_bytes()); // compression
            central_dir.extend_from_slice(&0u16.to_le_bytes()); // mod time
            central_dir.extend_from_slice(&0u16.to_le_bytes()); // mod date
            central_dir.extend_from_slice(&crc.to_le_bytes());
            central_dir.extend_from_slice(&size.to_le_bytes());
            central_dir.extend_from_slice(&size.to_le_bytes());
            central_dir.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
            central_dir.extend_from_slice(&0u16.to_le_bytes()); // extra len
            central_dir.extend_from_slice(&0u16.to_le_bytes()); // comment len
            central_dir.extend_from_slice(&0u16.to_le_bytes()); // disk start
            central_dir.extend_from_slice(&0u16.to_le_bytes()); // internal attrs
            central_dir.extend_from_slice(&0u32.to_le_bytes()); // external attrs
            central_dir.extend_from_slice(&offsets[i].to_le_bytes());
            central_dir.extend_from_slice(name_bytes);
        }

        let cd_offset = archive.len() as u32;
        archive.extend_from_slice(&central_dir);

        // End of central directory
        archive.extend_from_slice(&0x06054b50u32.to_le_bytes());
        archive.extend_from_slice(&0u16.to_le_bytes()); // disk number
        archive.extend_from_slice(&0u16.to_le_bytes()); // disk with CD
        archive.extend_from_slice(&(self.pages.len() as u16).to_le_bytes());
        archive.extend_from_slice(&(self.pages.len() as u16).to_le_bytes());
        archive.extend_from_slice(&(central_dir.len() as u32).to_le_bytes());
        archive.extend_from_slice(&cd_offset.to_le_bytes());
        archive.extend_from_slice(&0u16.to_le_bytes()); // comment len

        let _ = std::fs::write(path, &archive);
    }

    /// Compute CRC-32 for ZIP file entries
    fn crc32_compute(data: &[u8]) -> u32 {
        let mut crc: u32 = 0xFFFFFFFF;
        for &byte in data {
            crc ^= byte as u32;
            for _ in 0..8 {
                if crc & 1 != 0 {
                    crc = (crc >> 1) ^ 0xEDB88320;
                } else {
                    crc >>= 1;
                }
            }
        }
        !crc
    }

    /// Write image output
    ///
    /// Generates valid image files for each page using the page's
    /// dimensions. Without a full rasterizer, pages are rendered as
    /// white (zero-content) images with correct format headers.
    fn write_image(&self) {
        for (i, page) in self.pages.iter().enumerate() {
            if let Some(ref path) = self.path {
                let ext = self.format.extension();
                let page_path = if self.pages.len() > 1 {
                    let base = path.trim_end_matches(&format!(".{}", ext));
                    format!("{}_{}.{}", base, i + 1, ext)
                } else {
                    path.clone()
                };

                let width = (page.mediabox.x1 - page.mediabox.x0).max(1.0) as u32;
                let height = (page.mediabox.y1 - page.mediabox.y0).max(1.0) as u32;

                let image_data = match self.format {
                    WriterFormat::Png => Self::generate_png(width, height),
                    WriterFormat::Pam => Self::generate_pam(width, height),
                    WriterFormat::Pnm | WriterFormat::Ppm => Self::generate_ppm(width, height),
                    WriterFormat::Pgm => Self::generate_pgm(width, height),
                    WriterFormat::Pbm => Self::generate_pbm(width, height),
                    _ => {
                        // JPEG, PKM, and other formats: generate PNG as
                        // a reasonable fallback since we lack dedicated
                        // encoders for every format
                        Self::generate_png(width, height)
                    }
                };

                let _ = std::fs::write(page_path, &image_data);
            }
        }
    }

    /// Generate a valid minimal PNG: white RGB image of given dimensions.
    ///
    /// Builds the file byte-by-byte (signature, IHDR, IDAT, IEND) so we
    /// depend only on `flate2`, not on the `image` crate at runtime.
    fn generate_png(width: u32, height: u32) -> Vec<u8> {
        use flate2::Compression;
        use flate2::write::ZlibEncoder;
        use std::io::Write;

        let mut out = Vec::new();

        // PNG signature
        out.extend_from_slice(&[137, 80, 78, 71, 13, 10, 26, 10]);

        // --- IHDR chunk ---
        let mut ihdr_data = Vec::with_capacity(13);
        ihdr_data.extend_from_slice(&width.to_be_bytes());
        ihdr_data.extend_from_slice(&height.to_be_bytes());
        ihdr_data.push(8); // bit depth
        ihdr_data.push(2); // color type: RGB
        ihdr_data.push(0); // compression
        ihdr_data.push(0); // filter
        ihdr_data.push(0); // interlace
        Self::write_png_chunk(&mut out, b"IHDR", &ihdr_data);

        // --- IDAT chunk ---
        // Build raw image rows: each row starts with filter byte 0 (None)
        // followed by width * 3 bytes of white (0xFF) pixels.
        let row_bytes = 1 + (width as usize) * 3; // filter byte + RGB
        let mut raw = Vec::with_capacity(row_bytes * height as usize);
        for _ in 0..height {
            raw.push(0); // filter: None
            raw.extend(std::iter::repeat_n(0xFF_u8, width as usize * 3));
        }

        // Deflate-compress the raw data
        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast());
        let _ = encoder.write_all(&raw);
        let compressed = encoder.finish().unwrap_or_default();
        Self::write_png_chunk(&mut out, b"IDAT", &compressed);

        // --- IEND chunk ---
        Self::write_png_chunk(&mut out, b"IEND", &[]);

        out
    }

    /// Write a single PNG chunk: length(4) + type(4) + data + CRC(4).
    fn write_png_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) {
        out.extend_from_slice(&(data.len() as u32).to_be_bytes());
        out.extend_from_slice(chunk_type);
        out.extend_from_slice(data);

        // CRC covers chunk type + data
        let crc = Self::png_crc32(chunk_type, data);
        out.extend_from_slice(&crc.to_be_bytes());
    }

    /// Compute CRC-32 over chunk_type ++ data (PNG uses ISO 3309 / ITU-T V.42).
    fn png_crc32(chunk_type: &[u8], data: &[u8]) -> u32 {
        // Build the standard CRC table on the stack (256 entries).
        let mut table = [0u32; 256];
        for n in 0..256u32 {
            let mut c = n;
            for _ in 0..8 {
                if c & 1 != 0 {
                    c = 0xEDB8_8320 ^ (c >> 1);
                } else {
                    c >>= 1;
                }
            }
            table[n as usize] = c;
        }

        let mut crc: u32 = 0xFFFF_FFFF;
        for &b in chunk_type.iter().chain(data.iter()) {
            crc = table[((crc ^ b as u32) & 0xFF) as usize] ^ (crc >> 8);
        }
        crc ^ 0xFFFF_FFFF
    }

    /// Generate a valid PAM (Portable Arbitrary Map) image.
    ///
    /// PAM is the most general Netpbm format and uses a text header
    /// followed by raw pixel data.
    fn generate_pam(width: u32, height: u32) -> Vec<u8> {
        let header = format!(
            "P7\nWIDTH {}\nHEIGHT {}\nDEPTH 3\nMAXVAL 255\nTUPLTYPE RGB\nENDHDR\n",
            width, height
        );
        let pixel_count = (width as usize) * (height as usize) * 3;
        let mut data = Vec::with_capacity(header.len() + pixel_count);
        data.extend_from_slice(header.as_bytes());
        // White pixels
        data.extend(std::iter::repeat_n(0xFF_u8, pixel_count));
        data
    }

    /// Generate a valid PPM (Portable Pixel Map, P6 binary) image.
    fn generate_ppm(width: u32, height: u32) -> Vec<u8> {
        let header = format!("P6\n{} {}\n255\n", width, height);
        let pixel_count = (width as usize) * (height as usize) * 3;
        let mut data = Vec::with_capacity(header.len() + pixel_count);
        data.extend_from_slice(header.as_bytes());
        data.extend(std::iter::repeat_n(0xFF_u8, pixel_count));
        data
    }

    /// Generate a valid PGM (Portable Gray Map, P5 binary) image.
    fn generate_pgm(width: u32, height: u32) -> Vec<u8> {
        let header = format!("P5\n{} {}\n255\n", width, height);
        let pixel_count = (width as usize) * (height as usize);
        let mut data = Vec::with_capacity(header.len() + pixel_count);
        data.extend_from_slice(header.as_bytes());
        data.extend(std::iter::repeat_n(0xFF_u8, pixel_count));
        data
    }

    /// Generate a valid PBM (Portable Bitmap, P4 binary) image.
    ///
    /// PBM packs 8 pixels per byte. A 0 bit = white, 1 bit = black.
    /// All-white means all zero bytes.
    fn generate_pbm(width: u32, height: u32) -> Vec<u8> {
        let header = format!("P4\n{} {}\n", width, height);
        let row_bytes = (width as usize + 7) / 8;
        let pixel_bytes = row_bytes * height as usize;
        let mut data = Vec::with_capacity(header.len() + pixel_bytes);
        data.extend_from_slice(header.as_bytes());
        // 0 bits = white in PBM
        data.extend(std::iter::repeat_n(0x00_u8, pixel_bytes));
        data
    }
}

/// Writer device (borrowed from begin_page)
pub struct WriterDevice {
    pub writer: Handle,
    pub page_index: i32,
}

// ============================================================================
// FFI Functions - Option Utilities
// ============================================================================

/// Check if options string has a specific key
#[unsafe(no_mangle)]
pub extern "C" fn fz_has_option(
    _ctx: Handle,
    opts: *const c_char,
    key: *const c_char,
    val: *mut *const c_char,
) -> i32 {
    if opts.is_null() || key.is_null() {
        return 0;
    }

    let opts_str = unsafe {
        match CStr::from_ptr(opts).to_str() {
            Ok(s) => s,
            Err(_) => return 0,
        }
    };

    let key_str = unsafe {
        match CStr::from_ptr(key).to_str() {
            Ok(s) => s,
            Err(_) => return 0,
        }
    };

    let options = WriterOptions::parse(opts_str);

    if let Some(value) = options.get(key_str) {
        if !val.is_null() {
            // Store the value pointer (caller must not free)
            if let Ok(cstr) = CString::new(value.as_str()) {
                unsafe {
                    *val = cstr.into_raw();
                }
            }
        }
        1
    } else {
        0
    }
}

/// Check if option 'a' matches reference option 'b'
#[unsafe(no_mangle)]
pub extern "C" fn fz_option_eq(a: *const c_char, b: *const c_char) -> i32 {
    if a.is_null() || b.is_null() {
        return 0;
    }

    let a_str = unsafe {
        match CStr::from_ptr(a).to_str() {
            Ok(s) => s,
            Err(_) => return 0,
        }
    };

    let b_str = unsafe {
        match CStr::from_ptr(b).to_str() {
            Ok(s) => s,
            Err(_) => return 0,
        }
    };

    // a could be "foo" or "foo,bar..." but b must be exactly "foo"
    let a_first = a_str.split(',').next().unwrap_or("");
    if a_first == b_str { 1 } else { 0 }
}

/// Copy an option value to destination buffer
#[unsafe(no_mangle)]
pub extern "C" fn fz_copy_option(
    _ctx: Handle,
    val: *const c_char,
    dest: *mut c_char,
    maxlen: usize,
) -> usize {
    if val.is_null() || dest.is_null() || maxlen == 0 {
        return 0;
    }

    let val_str = unsafe {
        match CStr::from_ptr(val).to_str() {
            Ok(s) => s,
            Err(_) => return 0,
        }
    };

    // Find end of value (comma or end of string)
    let value_end = val_str.find(',').unwrap_or(val_str.len());
    let value = &val_str[..value_end];

    let bytes = value.as_bytes();
    let copy_len = bytes.len().min(maxlen - 1);

    unsafe {
        std::ptr::copy_nonoverlapping(bytes.as_ptr(), dest as *mut u8, copy_len);
        *dest.add(copy_len) = 0; // Null terminator
    }

    // Return bytes that didn't fit
    if bytes.len() >= maxlen {
        bytes.len() + 1 - maxlen
    } else {
        0
    }
}

// ============================================================================
// FFI Functions - Document Writer Creation
// ============================================================================

/// Create a new document writer by format
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_document_writer(
    _ctx: Handle,
    path: *const c_char,
    format: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let format_str = if format.is_null() {
        // Try to infer from path extension
        path_str
            .as_ref()
            .and_then(|p| p.rsplit('.').next())
            .unwrap_or("pdf")
    } else {
        unsafe { CStr::from_ptr(format).to_str().unwrap_or("pdf") }
    };

    let writer_format = WriterFormat::from_str(format_str).unwrap_or(WriterFormat::Pdf);

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(writer_format, path_str, opts);
    WRITERS.insert(writer)
}

/// Create document writer with output handle
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_document_writer_with_output(
    _ctx: Handle,
    out: Handle,
    format: *const c_char,
    options: *const c_char,
) -> Handle {
    let format_str = if format.is_null() {
        "pdf"
    } else {
        unsafe { CStr::from_ptr(format).to_str().unwrap_or("pdf") }
    };

    let writer_format = WriterFormat::from_str(format_str).unwrap_or(WriterFormat::Pdf);

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::with_output(writer_format, out, opts);
    WRITERS.insert(writer)
}

/// Create document writer with buffer handle
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_document_writer_with_buffer(
    _ctx: Handle,
    buf: Handle,
    format: *const c_char,
    options: *const c_char,
) -> Handle {
    let format_str = if format.is_null() {
        "pdf"
    } else {
        unsafe { CStr::from_ptr(format).to_str().unwrap_or("pdf") }
    };

    let writer_format = WriterFormat::from_str(format_str).unwrap_or(WriterFormat::Pdf);

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::with_buffer(writer_format, buf, opts);
    WRITERS.insert(writer)
}

// ============================================================================
// FFI Functions - Format-Specific Writers
// ============================================================================

/// Create PDF writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_pdf_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Pdf, path_str, opts);
    WRITERS.insert(writer)
}

/// Create PDF writer with output
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_pdf_writer_with_output(
    _ctx: Handle,
    out: Handle,
    options: *const c_char,
) -> Handle {
    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::with_output(WriterFormat::Pdf, out, opts);
    WRITERS.insert(writer)
}

/// Create SVG writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_svg_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Svg, path_str, opts);
    WRITERS.insert(writer)
}

/// Create SVG writer with output
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_svg_writer_with_output(
    _ctx: Handle,
    out: Handle,
    options: *const c_char,
) -> Handle {
    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::with_output(WriterFormat::Svg, out, opts);
    WRITERS.insert(writer)
}

/// Create text writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_text_writer(
    _ctx: Handle,
    format: *const c_char,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    // Format can be "text", "html", "xhtml"
    let format_str = if format.is_null() {
        "text"
    } else {
        unsafe { CStr::from_ptr(format).to_str().unwrap_or("text") }
    };

    let writer_format = match format_str {
        "html" => WriterFormat::Html,
        "xhtml" => WriterFormat::Xhtml,
        _ => WriterFormat::Text,
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(writer_format, path_str, opts);
    WRITERS.insert(writer)
}

/// Create text writer with output
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_text_writer_with_output(
    _ctx: Handle,
    format: *const c_char,
    out: Handle,
    options: *const c_char,
) -> Handle {
    let format_str = if format.is_null() {
        "text"
    } else {
        unsafe { CStr::from_ptr(format).to_str().unwrap_or("text") }
    };

    let writer_format = match format_str {
        "html" => WriterFormat::Html,
        "xhtml" => WriterFormat::Xhtml,
        _ => WriterFormat::Text,
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::with_output(writer_format, out, opts);
    WRITERS.insert(writer)
}

/// Create ODT writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_odt_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Odt, path_str, opts);
    WRITERS.insert(writer)
}

/// Create ODT writer with output
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_odt_writer_with_output(
    _ctx: Handle,
    out: Handle,
    options: *const c_char,
) -> Handle {
    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::with_output(WriterFormat::Odt, out, opts);
    WRITERS.insert(writer)
}

/// Create DOCX writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_docx_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Docx, path_str, opts);
    WRITERS.insert(writer)
}

/// Create DOCX writer with output
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_docx_writer_with_output(
    _ctx: Handle,
    out: Handle,
    options: *const c_char,
) -> Handle {
    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::with_output(WriterFormat::Docx, out, opts);
    WRITERS.insert(writer)
}

/// Create PS writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_ps_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Ps, path_str, opts);
    WRITERS.insert(writer)
}

/// Create PS writer with output
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_ps_writer_with_output(
    _ctx: Handle,
    out: Handle,
    options: *const c_char,
) -> Handle {
    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::with_output(WriterFormat::Ps, out, opts);
    WRITERS.insert(writer)
}

/// Create PCL writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_pcl_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Pcl, path_str, opts);
    WRITERS.insert(writer)
}

/// Create PCL writer with output
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_pcl_writer_with_output(
    _ctx: Handle,
    out: Handle,
    options: *const c_char,
) -> Handle {
    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::with_output(WriterFormat::Pcl, out, opts);
    WRITERS.insert(writer)
}

/// Create PCLM writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_pclm_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Pclm, path_str, opts);
    WRITERS.insert(writer)
}

/// Create PCLM writer with output
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_pclm_writer_with_output(
    _ctx: Handle,
    out: Handle,
    options: *const c_char,
) -> Handle {
    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::with_output(WriterFormat::Pclm, out, opts);
    WRITERS.insert(writer)
}

/// Create PWG writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_pwg_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Pwg, path_str, opts);
    WRITERS.insert(writer)
}

/// Create PWG writer with output
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_pwg_writer_with_output(
    _ctx: Handle,
    out: Handle,
    options: *const c_char,
) -> Handle {
    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::with_output(WriterFormat::Pwg, out, opts);
    WRITERS.insert(writer)
}

/// Create CBZ writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_cbz_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Cbz, path_str, opts);
    WRITERS.insert(writer)
}

/// Create CBZ writer with output
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_cbz_writer_with_output(
    _ctx: Handle,
    out: Handle,
    options: *const c_char,
) -> Handle {
    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::with_output(WriterFormat::Cbz, out, opts);
    WRITERS.insert(writer)
}

/// Create CSV writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_csv_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Csv, path_str, opts);
    WRITERS.insert(writer)
}

/// Create CSV writer with output
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_csv_writer_with_output(
    _ctx: Handle,
    out: Handle,
    options: *const c_char,
) -> Handle {
    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::with_output(WriterFormat::Csv, out, opts);
    WRITERS.insert(writer)
}

/// Create PDF OCR writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_pdfocr_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Pdfocr, path_str, opts);
    WRITERS.insert(writer)
}

/// Create PDF OCR writer with output
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_pdfocr_writer_with_output(
    _ctx: Handle,
    out: Handle,
    options: *const c_char,
) -> Handle {
    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::with_output(WriterFormat::Pdfocr, out, opts);
    WRITERS.insert(writer)
}

// ============================================================================
// FFI Functions - Pixmap Writers
// ============================================================================

/// Create PNG pixmap writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_png_pixmap_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Png, path_str, opts);
    WRITERS.insert(writer)
}

/// Create JPEG pixmap writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_jpeg_pixmap_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Jpeg, path_str, opts);
    WRITERS.insert(writer)
}

/// Create PAM pixmap writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_pam_pixmap_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Pam, path_str, opts);
    WRITERS.insert(writer)
}

/// Create PNM pixmap writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_pnm_pixmap_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Pnm, path_str, opts);
    WRITERS.insert(writer)
}

/// Create PGM pixmap writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_pgm_pixmap_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Pgm, path_str, opts);
    WRITERS.insert(writer)
}

/// Create PPM pixmap writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_ppm_pixmap_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Ppm, path_str, opts);
    WRITERS.insert(writer)
}

/// Create PBM pixmap writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_pbm_pixmap_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Pbm, path_str, opts);
    WRITERS.insert(writer)
}

/// Create PKM pixmap writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_pkm_pixmap_writer(
    _ctx: Handle,
    path: *const c_char,
    options: *const c_char,
) -> Handle {
    let path_str = if path.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(path).to_str().ok().map(String::from) }
    };

    let opts = if options.is_null() {
        WriterOptions::default()
    } else {
        let opts_str = unsafe { CStr::from_ptr(options).to_str().unwrap_or("") };
        WriterOptions::parse(opts_str)
    };

    let writer = DocumentWriter::new(WriterFormat::Pkm, path_str, opts);
    WRITERS.insert(writer)
}

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

/// Begin writing a page
#[unsafe(no_mangle)]
pub extern "C" fn fz_begin_page(
    _ctx: Handle,
    wri: Handle,
    mediabox_x0: f32,
    mediabox_y0: f32,
    mediabox_x1: f32,
    mediabox_y1: f32,
) -> Handle {
    let mediabox = Rect {
        x0: mediabox_x0,
        y0: mediabox_y0,
        x1: mediabox_x1,
        y1: mediabox_y1,
    };

    if let Some(writer_arc) = WRITERS.get(wri) {
        let mut writer = writer_arc.lock().unwrap();
        if writer.begin_page(mediabox) {
            // Create a device handle for this page
            let device = WriterDevice {
                writer: wri,
                page_index: writer.page_count,
            };
            let dev_handle = WRITER_DEVICES.insert(device);
            writer.current_device = Some(dev_handle);
            return dev_handle;
        }
    }
    0
}

/// End writing a page
#[unsafe(no_mangle)]
pub extern "C" fn fz_end_page(_ctx: Handle, wri: Handle) {
    if let Some(writer_arc) = WRITERS.get(wri) {
        let mut writer = writer_arc.lock().unwrap();
        if let Some(dev_handle) = writer.current_device {
            WRITER_DEVICES.remove(dev_handle);
        }
        writer.end_page();
    }
}

/// Write a full document
///
/// Iterates through every page in the source document, extracts its
/// actual bounds, and writes a corresponding page into the writer.
/// Content streams are not re-rendered (that would require a full
/// rasteriser), but the output preserves the correct page count and
/// per-page dimensions.
#[unsafe(no_mangle)]
pub extern "C" fn fz_write_document(_ctx: Handle, wri: Handle, doc: Handle) {
    // Use the FFI function to get the page count so we don't depend
    // on private struct fields.
    let count = crate::ffi::document::fz_count_pages(0, doc);
    if count <= 0 {
        return;
    }

    // Collect page information while holding the document lock, then
    // release it before acquiring the writer lock to avoid deadlocks.
    let page_bounds: Vec<Rect> = {
        let doc_arc = match crate::ffi::DOCUMENTS.get(doc) {
            Some(d) => d,
            None => return,
        };
        let doc_guard = match doc_arc.lock() {
            Ok(g) => g,
            Err(_) => return,
        };

        // Try to extract per-page MediaBox values from the raw PDF data.
        // Falls back to US Letter (612x792) when parsing fails.
        let default_rect = Rect {
            x0: 0.0,
            y0: 0.0,
            x1: 612.0,
            y1: 792.0,
        };

        let data = doc_guard.data();
        let parsed = parse_mediaboxes_from_pdf(data, count as usize);

        (0..count as usize)
            .map(|i| parsed.get(i).copied().unwrap_or(default_rect))
            .collect()
    };

    // Now write each page into the writer.
    if let Some(writer_arc) = WRITERS.get(wri) {
        let mut writer = writer_arc.lock().unwrap();
        for mediabox in &page_bounds {
            writer.begin_page(*mediabox);
            writer.end_page();
        }
    }
}

/// Parse MediaBox arrays from raw PDF bytes.
///
/// Scans for `/Type /Page` dictionaries (skipping `/Type /Pages`) and
/// extracts the four-number `/MediaBox [ x0 y0 x1 y1 ]` that follows.
/// Returns up to `max_pages` results.
fn parse_mediaboxes_from_pdf(data: &[u8], max_pages: usize) -> Vec<Rect> {
    let mut results = Vec::with_capacity(max_pages);
    let page_pattern = b"/Type /Page";

    let mut pos = 0;
    while pos + page_pattern.len() < data.len() && results.len() < max_pages {
        // Find next /Type /Page
        if let Some(offset) = find_bytes(&data[pos..], page_pattern) {
            let abs = pos + offset;
            let after = abs + page_pattern.len();

            // Make sure this is /Type /Page and NOT /Type /Pages
            if data.get(after) == Some(&b's') {
                pos = after;
                continue;
            }

            // Search for /MediaBox within the surrounding dictionary
            // (look up to 512 bytes forward from /Type /Page)
            let search_end = (after + 512).min(data.len());
            if let Some(mb) = extract_mediabox(&data[abs..search_end]) {
                results.push(mb);
            } else {
                // Could not parse MediaBox; use US Letter default
                results.push(Rect {
                    x0: 0.0,
                    y0: 0.0,
                    x1: 612.0,
                    y1: 792.0,
                });
            }

            pos = after;
        } else {
            break;
        }
    }

    results
}

/// Find `needle` in `haystack`, returning the byte offset of the match.
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
    haystack.windows(needle.len()).position(|w| w == needle)
}

/// Extract a `/MediaBox [ x0 y0 x1 y1 ]` from a slice that starts at
/// or near a page dictionary.  Returns `None` if parsing fails.
fn extract_mediabox(data: &[u8]) -> Option<Rect> {
    let pattern = b"/MediaBox";
    let offset = find_bytes(data, pattern)?;
    let after = offset + pattern.len();

    // Skip whitespace, then expect '['
    let rest = &data[after..];
    let mut i = 0;
    while i < rest.len() && rest[i].is_ascii_whitespace() {
        i += 1;
    }
    if i >= rest.len() || rest[i] != b'[' {
        return None;
    }
    i += 1; // skip '['

    // Parse four numbers
    let mut nums = [0.0f32; 4];
    for num in &mut nums {
        // skip whitespace
        while i < rest.len() && rest[i].is_ascii_whitespace() {
            i += 1;
        }
        let start = i;
        while i < rest.len() && (rest[i].is_ascii_digit() || rest[i] == b'.' || rest[i] == b'-') {
            i += 1;
        }
        if start == i {
            return None;
        }
        let s = std::str::from_utf8(&rest[start..i]).ok()?;
        *num = s.parse::<f32>().ok()?;
    }

    Some(Rect {
        x0: nums[0],
        y0: nums[1],
        x1: nums[2],
        y1: nums[3],
    })
}

/// Close the document writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_close_document_writer(_ctx: Handle, wri: Handle) {
    if let Some(writer_arc) = WRITERS.get(wri) {
        let mut writer = writer_arc.lock().unwrap();
        writer.close();
    }
}

/// Drop the document writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_drop_document_writer(_ctx: Handle, wri: Handle) {
    // Close if not already closed
    if let Some(writer_arc) = WRITERS.get(wri) {
        {
            let mut writer = writer_arc.lock().unwrap();
            if writer.state != WriterState::Closed {
                writer.close();
            }
        }
    }
    WRITERS.remove(wri);
}

// ============================================================================
// FFI Functions - Writer Info
// ============================================================================

/// Get writer format
#[unsafe(no_mangle)]
pub extern "C" fn fz_document_writer_format(wri: Handle) -> i32 {
    if let Some(writer_arc) = WRITERS.get(wri) {
        let writer = writer_arc.lock().unwrap();
        writer.format as i32
    } else {
        -1
    }
}

/// Get writer page count
#[unsafe(no_mangle)]
pub extern "C" fn fz_document_writer_page_count(wri: Handle) -> i32 {
    if let Some(writer_arc) = WRITERS.get(wri) {
        let writer = writer_arc.lock().unwrap();
        writer.page_count
    } else {
        0
    }
}

/// Check if writer is closed
#[unsafe(no_mangle)]
pub extern "C" fn fz_document_writer_is_closed(wri: Handle) -> i32 {
    if let Some(writer_arc) = WRITERS.get(wri) {
        let writer = writer_arc.lock().unwrap();
        if writer.state == WriterState::Closed {
            1
        } else {
            0
        }
    } else {
        1
    }
}

// ============================================================================
// FFI Functions - OCR Progress Callback
// ============================================================================

/// Global storage for OCR progress callbacks
static OCR_PROGRESS_CALLBACKS: LazyLock<Mutex<HashMap<Handle, OcrProgressCallback>>> =
    LazyLock::new(|| Mutex::new(HashMap::new()));

/// OCR progress callback wrapper
pub struct OcrProgressCallback {
    pub callback: extern "C" fn(Handle, *mut std::ffi::c_void, i32, i32) -> i32,
    pub user_data: *mut std::ffi::c_void,
}

// Safety: callback is a function pointer, user_data is externally managed
unsafe impl Send for OcrProgressCallback {}
unsafe impl Sync for OcrProgressCallback {}

/// Set OCR progress callback for writer
#[unsafe(no_mangle)]
pub extern "C" fn fz_pdfocr_writer_set_progress(
    _ctx: Handle,
    wri: Handle,
    progress: extern "C" fn(Handle, *mut std::ffi::c_void, i32, i32) -> i32,
    user_data: *mut std::ffi::c_void,
) {
    let mut callbacks = OCR_PROGRESS_CALLBACKS.lock().unwrap();
    callbacks.insert(
        wri,
        OcrProgressCallback {
            callback: progress,
            user_data,
        },
    );
}

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

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

    #[test]
    fn test_writer_format_from_str() {
        assert_eq!(WriterFormat::from_str("pdf"), Some(WriterFormat::Pdf));
        assert_eq!(WriterFormat::from_str("PDF"), Some(WriterFormat::Pdf));
        assert_eq!(WriterFormat::from_str("svg"), Some(WriterFormat::Svg));
        assert_eq!(WriterFormat::from_str("png"), Some(WriterFormat::Png));
        assert_eq!(WriterFormat::from_str("jpeg"), Some(WriterFormat::Jpeg));
        assert_eq!(WriterFormat::from_str("jpg"), Some(WriterFormat::Jpeg));
        assert_eq!(WriterFormat::from_str("unknown"), None);
    }

    #[test]
    fn test_writer_format_extension() {
        assert_eq!(WriterFormat::Pdf.extension(), "pdf");
        assert_eq!(WriterFormat::Svg.extension(), "svg");
        assert_eq!(WriterFormat::Png.extension(), "png");
        assert_eq!(WriterFormat::Jpeg.extension(), "jpg");
    }

    #[test]
    fn test_writer_format_multipage() {
        assert!(WriterFormat::Pdf.is_multipage());
        assert!(WriterFormat::Svg.is_multipage());
        assert!(!WriterFormat::Png.is_multipage());
        assert!(!WriterFormat::Jpeg.is_multipage());
    }

    #[test]
    fn test_writer_options_parse() {
        let opts = WriterOptions::parse("resolution=300,quality=85,linearize");
        assert_eq!(opts.resolution, Some(300.0));
        assert_eq!(opts.quality, Some(85));
        assert!(opts.linearize);
        assert!(!opts.encrypt);
    }

    #[test]
    fn test_writer_options_has() {
        let opts = WriterOptions::parse("foo=bar,baz");
        assert!(opts.has("foo"));
        assert!(opts.has("baz"));
        assert!(!opts.has("qux"));
    }

    #[test]
    fn test_new_document_writer() {
        let ctx = 1;
        let path = CString::new("/tmp/test.pdf").unwrap();
        let format = CString::new("pdf").unwrap();

        let wri = fz_new_document_writer(ctx, path.as_ptr(), format.as_ptr(), std::ptr::null());
        assert!(wri > 0);

        fz_drop_document_writer(ctx, wri);
    }

    #[test]
    fn test_pdf_writer() {
        let ctx = 1;
        let path = CString::new("/tmp/test_pdf.pdf").unwrap();

        let wri = fz_new_pdf_writer(ctx, path.as_ptr(), std::ptr::null());
        assert!(wri > 0);

        // Begin a page
        let dev = fz_begin_page(ctx, wri, 0.0, 0.0, 612.0, 792.0);
        assert!(dev > 0);

        // End the page
        fz_end_page(ctx, wri);

        // Close and drop
        fz_close_document_writer(ctx, wri);
        fz_drop_document_writer(ctx, wri);
    }

    #[test]
    fn test_svg_writer() {
        let ctx = 1;
        let path = CString::new("/tmp/test.svg").unwrap();

        let wri = fz_new_svg_writer(ctx, path.as_ptr(), std::ptr::null());
        assert!(wri > 0);

        fz_drop_document_writer(ctx, wri);
    }

    #[test]
    fn test_text_writer() {
        let ctx = 1;
        let format = CString::new("text").unwrap();
        let path = CString::new("/tmp/test.txt").unwrap();

        let wri = fz_new_text_writer(ctx, format.as_ptr(), path.as_ptr(), std::ptr::null());
        assert!(wri > 0);

        fz_drop_document_writer(ctx, wri);
    }

    #[test]
    fn test_png_writer() {
        let ctx = 1;
        let path = CString::new("/tmp/test.png").unwrap();

        let wri = fz_new_png_pixmap_writer(ctx, path.as_ptr(), std::ptr::null());
        assert!(wri > 0);

        fz_drop_document_writer(ctx, wri);
    }

    #[test]
    fn test_option_eq() {
        let a1 = CString::new("foo").unwrap();
        let a2 = CString::new("foo,bar").unwrap();
        let b = CString::new("foo").unwrap();

        assert_eq!(fz_option_eq(a1.as_ptr(), b.as_ptr()), 1);
        assert_eq!(fz_option_eq(a2.as_ptr(), b.as_ptr()), 1);

        let c = CString::new("bar").unwrap();
        assert_eq!(fz_option_eq(a1.as_ptr(), c.as_ptr()), 0);
    }

    #[test]
    fn test_copy_option() {
        let ctx = 1;
        let val = CString::new("hello,world").unwrap();
        let mut dest = [0i8; 32];

        let overflow = fz_copy_option(ctx, val.as_ptr(), dest.as_mut_ptr(), 32);
        assert_eq!(overflow, 0);

        let result = unsafe { CStr::from_ptr(dest.as_ptr()) };
        assert_eq!(result.to_str().unwrap(), "hello");
    }

    #[test]
    fn test_writer_page_count() {
        let ctx = 1;

        let wri = fz_new_pdf_writer(ctx, std::ptr::null(), std::ptr::null());
        assert_eq!(fz_document_writer_page_count(wri), 0);

        fz_begin_page(ctx, wri, 0.0, 0.0, 612.0, 792.0);
        fz_end_page(ctx, wri);
        assert_eq!(fz_document_writer_page_count(wri), 1);

        fz_begin_page(ctx, wri, 0.0, 0.0, 612.0, 792.0);
        fz_end_page(ctx, wri);
        assert_eq!(fz_document_writer_page_count(wri), 2);

        fz_drop_document_writer(ctx, wri);
    }

    #[test]
    fn test_writer_is_closed() {
        let ctx = 1;

        let wri = fz_new_pdf_writer(ctx, std::ptr::null(), std::ptr::null());
        assert_eq!(fz_document_writer_is_closed(wri), 0);

        fz_close_document_writer(ctx, wri);
        assert_eq!(fz_document_writer_is_closed(wri), 1);

        fz_drop_document_writer(ctx, wri);
    }

    #[test]
    fn test_all_format_writers() {
        let ctx = 1;

        // Test each format-specific writer
        let writers = vec![
            fz_new_pdf_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_svg_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_odt_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_docx_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_ps_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_pcl_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_pclm_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_pwg_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_cbz_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_csv_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_pdfocr_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_png_pixmap_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_jpeg_pixmap_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_pam_pixmap_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_pnm_pixmap_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_pgm_pixmap_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_ppm_pixmap_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_pbm_pixmap_writer(ctx, std::ptr::null(), std::ptr::null()),
            fz_new_pkm_pixmap_writer(ctx, std::ptr::null(), std::ptr::null()),
        ];

        for wri in writers {
            assert!(wri > 0);
            fz_drop_document_writer(ctx, wri);
        }
    }
}
