#![forbid(unsafe_code)]
#![doc = "Core types and utilities for rpdfium — a faithful Rust port of PDFium."]
//!
//! This crate provides the foundational types shared across all rpdfium crates:
//!
//! - [`PdfError`] and [`ParseError`] — structured error types
//! - [`PdfString`] — encoding-aware PDF string with PDFDocEncoding support
//! - [`Name`] — PDF name objects with predefined constants
//! - [`Point`], [`Size`], [`Vector2D`], [`Rect`], [`RectI`], [`Matrix`] — geometric types
//! - [`PdfSource`] — trait for PDF source data abstraction
//! - [`ParsingMode`] and [`OpenOptions`] — configuration types
//! - Security limit constants in the [`fx_system`] module
//! - [`Diagnostic`] types for structured error reporting

pub mod bytestring;
pub mod cfx_bitstream;
pub mod constants;
pub mod diagnostic;
pub mod error;
pub mod fx_bidi;
pub mod fx_coordinates;
pub mod fx_stream;
pub mod fx_system;
pub mod name;
pub mod widestring;

// Re-export primary types at the crate root for convenience.
pub use bytestring::{PdfString, PdfStringEncoding, pdfdoc_to_char};
pub use diagnostic::{DiagCategory, DiagLocation, Diagnostic, DiagnosticCollector, Severity};
pub use error::{ObjectId, ParseError, PdfError, PdfResult};
pub use fx_bidi::{
    BidiChar, BidiDirection, BidiSegment, BidiString, Direction, Segment, bidi_class_of,
    mirror_char,
};
pub use fx_coordinates::{Matrix, Point, Rect, RectI, Size, Vector2D};
pub use fx_stream::{CountingWriter, MemoryStream, PdfSource, PdfWrite};
pub use fx_system::is_float_zero;
pub use name::Name;
pub use widestring::WideString;

/// Parsing mode controlling how strictly the PDF specification is enforced.
///
/// - [`Strict`](ParsingMode::Strict): Return errors on spec violations. Useful
///   for testing, validation, and PDF conformance checking.
/// - [`Lenient`](ParsingMode::Lenient) (default): Accept malformed input
///   wherever possible, recovering gracefully, and report issues via `tracing`.
///   This replicates PDFium's permissive behavior.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ParsingMode {
    /// Return errors on spec violations.
    Strict,
    /// Recover where possible, log warnings via tracing.
    #[default]
    Lenient,
}

impl std::fmt::Display for ParsingMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ParsingMode::Strict => write!(f, "strict"),
            ParsingMode::Lenient => write!(f, "lenient"),
        }
    }
}

/// Options for opening a PDF document.
///
/// All fields have sensible defaults via the [`Default`] implementation.
/// Soft limits can be adjusted for trusted inputs that exceed the defaults.
#[derive(Debug, Clone)]
pub struct OpenOptions {
    /// Parsing mode (default: [`ParsingMode::Lenient`]).
    pub parsing_mode: ParsingMode,

    /// Password for encrypted documents (default: `None`).
    pub password: Option<String>,

    /// Maximum decompressed stream size in bytes (default: 256 MB).
    ///
    /// Prevents decompression bombs.
    pub max_decompressed_stream_size: u64,

    /// Maximum compression ratio before aborting (default: 1000:1).
    ///
    /// A ratio exceeding this strongly indicates a zip bomb.
    pub max_compression_ratio: u32,

    /// Maximum content stream operators per page (default: 10,000,000).
    pub max_operators_per_page: u64,

    /// Maximum distance to scan for `endstream` keyword (default: 16 MB).
    pub max_endstream_scan_distance: u64,
}

impl OpenOptions {
    /// Set the parsing mode.
    pub fn with_parsing_mode(mut self, mode: ParsingMode) -> Self {
        self.parsing_mode = mode;
        self
    }

    /// Set the password for encrypted documents.
    pub fn with_password(mut self, password: impl Into<String>) -> Self {
        self.password = Some(password.into());
        self
    }

    /// Set the maximum decompressed stream size in bytes.
    pub fn with_max_decompressed_stream_size(mut self, size: u64) -> Self {
        self.max_decompressed_stream_size = size;
        self
    }

    /// Set the maximum compression ratio before aborting.
    pub fn with_max_compression_ratio(mut self, ratio: u32) -> Self {
        self.max_compression_ratio = ratio;
        self
    }

    /// Set the maximum content stream operators per page.
    pub fn with_max_operators_per_page(mut self, count: u64) -> Self {
        self.max_operators_per_page = count;
        self
    }

    /// Set the maximum distance to scan for `endstream`.
    pub fn with_max_endstream_scan_distance(mut self, distance: u64) -> Self {
        self.max_endstream_scan_distance = distance;
        self
    }
}

impl Default for OpenOptions {
    fn default() -> Self {
        Self {
            parsing_mode: ParsingMode::default(),
            password: None,
            max_decompressed_stream_size: fx_system::DEFAULT_MAX_DECOMPRESSED_STREAM_SIZE,
            max_compression_ratio: fx_system::DEFAULT_MAX_COMPRESSION_RATIO,
            max_operators_per_page: fx_system::DEFAULT_MAX_OPERATORS_PER_PAGE,
            max_endstream_scan_distance: fx_system::DEFAULT_MAX_ENDSTREAM_SCAN_DISTANCE,
        }
    }
}

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

    #[test]
    fn test_parsing_mode_default_is_lenient() {
        assert_eq!(ParsingMode::default(), ParsingMode::Lenient);
    }

    #[test]
    fn test_parsing_mode_display() {
        assert_eq!(format!("{}", ParsingMode::Strict), "strict");
        assert_eq!(format!("{}", ParsingMode::Lenient), "lenient");
    }

    #[test]
    fn test_open_options_default() {
        let opts = OpenOptions::default();
        assert_eq!(opts.parsing_mode, ParsingMode::Lenient);
        assert!(opts.password.is_none());
        assert_eq!(
            opts.max_decompressed_stream_size,
            fx_system::DEFAULT_MAX_DECOMPRESSED_STREAM_SIZE
        );
        assert_eq!(
            opts.max_compression_ratio,
            fx_system::DEFAULT_MAX_COMPRESSION_RATIO
        );
        assert_eq!(
            opts.max_operators_per_page,
            fx_system::DEFAULT_MAX_OPERATORS_PER_PAGE
        );
        assert_eq!(
            opts.max_endstream_scan_distance,
            fx_system::DEFAULT_MAX_ENDSTREAM_SCAN_DISTANCE
        );
    }

    #[test]
    fn test_open_options_custom() {
        let opts = OpenOptions {
            parsing_mode: ParsingMode::Strict,
            password: Some("secret".to_string()),
            max_decompressed_stream_size: 512 * 1024 * 1024,
            ..OpenOptions::default()
        };
        assert_eq!(opts.parsing_mode, ParsingMode::Strict);
        assert_eq!(opts.password.as_deref(), Some("secret"));
        assert_eq!(opts.max_decompressed_stream_size, 512 * 1024 * 1024);
    }

    #[test]
    fn test_open_options_builder_parsing_mode() {
        let opts = OpenOptions::default().with_parsing_mode(ParsingMode::Strict);
        assert_eq!(opts.parsing_mode, ParsingMode::Strict);
    }

    #[test]
    fn test_open_options_builder_password() {
        let opts = OpenOptions::default().with_password("secret");
        assert_eq!(opts.password.as_deref(), Some("secret"));
    }

    #[test]
    fn test_open_options_builder_max_decompressed_stream_size() {
        let opts = OpenOptions::default().with_max_decompressed_stream_size(1024);
        assert_eq!(opts.max_decompressed_stream_size, 1024);
    }

    #[test]
    fn test_open_options_builder_max_compression_ratio() {
        let opts = OpenOptions::default().with_max_compression_ratio(500);
        assert_eq!(opts.max_compression_ratio, 500);
    }

    #[test]
    fn test_open_options_builder_max_operators() {
        let opts = OpenOptions::default().with_max_operators_per_page(5000);
        assert_eq!(opts.max_operators_per_page, 5000);
    }

    #[test]
    fn test_open_options_builder_max_endstream_scan() {
        let opts = OpenOptions::default().with_max_endstream_scan_distance(8192);
        assert_eq!(opts.max_endstream_scan_distance, 8192);
    }

    #[test]
    fn test_open_options_builder_chaining() {
        let opts = OpenOptions::default()
            .with_parsing_mode(ParsingMode::Strict)
            .with_password("pass")
            .with_max_decompressed_stream_size(512)
            .with_max_compression_ratio(100)
            .with_max_operators_per_page(1000)
            .with_max_endstream_scan_distance(4096);
        assert_eq!(opts.parsing_mode, ParsingMode::Strict);
        assert_eq!(opts.password.as_deref(), Some("pass"));
        assert_eq!(opts.max_decompressed_stream_size, 512);
        assert_eq!(opts.max_compression_ratio, 100);
        assert_eq!(opts.max_operators_per_page, 1000);
        assert_eq!(opts.max_endstream_scan_distance, 4096);
    }

    #[test]
    fn test_parsing_mode_is_send_sync() {
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<ParsingMode>();
    }

    #[test]
    fn test_open_options_is_send_sync() {
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<OpenOptions>();
    }

    #[test]
    fn test_parsing_mode_equality() {
        assert_eq!(ParsingMode::Strict, ParsingMode::Strict);
        assert_eq!(ParsingMode::Lenient, ParsingMode::Lenient);
        assert_ne!(ParsingMode::Strict, ParsingMode::Lenient);
    }

    #[test]
    fn test_all_core_types_reexported() {
        // Verify key types are accessible from crate root
        let _: ObjectId = ObjectId {
            number: 1,
            generation: 0,
        };
        let _: PdfString = PdfString::from_bytes(vec![]);
        let _: Name = Name::from("Test");
        let _: Point = Point::new(0.0, 0.0);
        let _: Size = Size::new(0.0, 0.0);
        let _: Rect = Rect::new(0.0, 0.0, 1.0, 1.0);
        let _: Matrix = Matrix::identity();
        let _: ParsingMode = ParsingMode::default();
        let _: OpenOptions = OpenOptions::default();
        let _: DiagnosticCollector = DiagnosticCollector::new();
    }
}
