//! Error types for rpdfium.
//!
//! This module defines the error hierarchy used throughout the rpdfium crates.
//! All errors use [`thiserror`] for derivation and implement `Send + Sync`.

use std::fmt;

/// Unique identifier for a PDF indirect object.
///
/// PDF indirect objects are identified by a (number, generation) pair.
/// The generation number is incremented when an object is updated in an
/// incremental PDF update.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ObjectId {
    /// Object number (1-based in the PDF file).
    pub number: u32,
    /// Generation number (0 for newly created objects).
    pub generation: u16,
}

impl ObjectId {
    /// Create a new ObjectId.
    pub fn new(number: u32, generation: u16) -> Self {
        Self { number, generation }
    }
}

impl fmt::Display for ObjectId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {} R", self.number, self.generation)
    }
}

/// Top-level error type for rpdfium operations.
#[derive(Debug, thiserror::Error)]
pub enum PdfError {
    /// A syntax-level parse error occurred.
    #[error("parse error: {0}")]
    Parse(#[from] ParseError),

    /// A circular reference was detected during object resolution.
    #[error("circular reference detected at object {0}")]
    CircularReference(ObjectId),

    /// The recursion limit was exceeded during object resolution or traversal.
    #[error("recursion limit exceeded")]
    RecursionLimitExceeded,

    /// The referenced object does not exist in the cross-reference table.
    #[error("unknown object: {0}")]
    UnknownObject(ObjectId),

    /// The object is not a stream when a stream was expected.
    #[error("expected a stream object")]
    NotAStream,

    /// The decode filter chain exceeds the maximum allowed length.
    #[error("filter chain too long")]
    FilterChainTooLong,

    /// The image dimensions exceed the maximum allowed size.
    #[error("image too large")]
    ImageTooLarge,

    /// The decompressed stream size exceeds the maximum allowed size.
    #[error("stream too large")]
    StreamTooLarge,

    /// A decompression bomb was detected (compression ratio exceeds limit).
    #[error("compression bomb detected")]
    CompressionBombDetected,

    /// The number of content stream operators exceeds the maximum allowed.
    #[error("too many operators in content stream")]
    TooManyOperators,

    /// The `endstream` keyword could not be found within the scan distance.
    #[error("endstream scan failed")]
    EndstreamScanFailed,

    /// The PDF file header is invalid.
    #[error("invalid PDF header")]
    InvalidHeader,

    /// The cross-reference table is invalid.
    #[error("invalid cross-reference table")]
    InvalidXref,

    /// The PDF trailer is invalid.
    #[error("invalid trailer")]
    InvalidTrailer,

    /// A stream decode filter failed.
    #[error("stream decode error: {0}")]
    StreamDecodeError(String),

    /// An object stream is malformed.
    #[error("invalid object stream")]
    InvalidObjectStream,

    /// A PDF object is invalid or has an unexpected type.
    #[error("invalid object: {0}")]
    InvalidObject(String),

    /// The provided password is incorrect.
    #[error("invalid password")]
    InvalidPassword,

    /// The PDF uses an unsupported encryption scheme.
    #[error("unsupported encryption")]
    UnsupportedEncryption,

    /// The requested operation is not supported by rpdfium (excluded by design).
    #[error("not supported: {0}")]
    NotSupported(String),

    /// An I/O error occurred.
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),
}

/// Syntax-level parse error for PDF objects.
///
/// These errors represent problems at the PDF syntax level (tokens,
/// object structure, etc.) as opposed to higher-level semantic errors.
#[derive(Debug, Clone, thiserror::Error)]
pub enum ParseError {
    /// An unexpected token was encountered.
    #[error("unexpected token at offset {offset}: expected {expected}, found {found}")]
    UnexpectedToken {
        /// Byte offset in the source where the error occurred.
        offset: u64,
        /// Description of what was expected.
        expected: String,
        /// Description of what was found.
        found: String,
    },

    /// An unexpected end of input was reached.
    #[error("unexpected end of input at offset {offset}")]
    UnexpectedEof {
        /// Byte offset where the input ended.
        offset: u64,
    },

    /// A numeric value is out of the representable range.
    #[error("number out of range at offset {offset}")]
    NumberOutOfRange {
        /// Byte offset in the source where the error occurred.
        offset: u64,
    },

    /// A string literal is malformed (unbalanced parentheses, invalid escape, etc.).
    #[error("invalid string at offset {offset}: {detail}")]
    InvalidString {
        /// Byte offset in the source where the error occurred.
        offset: u64,
        /// Details about the problem.
        detail: String,
    },

    /// A name object contains invalid data.
    #[error("invalid name at offset {offset}: {detail}")]
    InvalidName {
        /// Byte offset in the source where the error occurred.
        offset: u64,
        /// Details about the problem.
        detail: String,
    },

    /// An invalid indirect object header (`N G obj`).
    #[error("invalid object header at offset {offset}")]
    InvalidObjectHeader {
        /// Byte offset in the source where the error occurred.
        offset: u64,
    },

    /// The `endobj` keyword is missing after an indirect object.
    #[error("missing endobj at offset {offset}")]
    MissingEndobj {
        /// Byte offset in the source where the error occurred.
        offset: u64,
    },

    /// A duplicate key was found in a dictionary.
    #[error("duplicate dictionary key: {key}")]
    DuplicateKey {
        /// The duplicated key name.
        key: String,
    },

    /// The `endstream` keyword is missing after stream data.
    #[error("missing endstream at offset {offset}")]
    MissingEndstream {
        /// Byte offset in the source where the error occurred.
        offset: u64,
    },

    /// A numeric literal is invalid (not a valid integer or real).
    #[error("invalid number at offset {offset}")]
    InvalidNumber {
        /// Byte offset in the source where the error occurred.
        offset: u64,
    },

    /// A cross-reference entry is malformed.
    #[error("invalid xref entry at offset {offset}")]
    InvalidXrefEntry {
        /// Byte offset in the source where the error occurred.
        offset: u64,
    },

    /// A generic parse error with a descriptive message.
    #[error("parse error at offset {offset}: {message}")]
    Other {
        /// Byte offset in the source where the error occurred.
        offset: u64,
        /// Description of the error.
        message: String,
    },
}

/// A specialized `Result` type for rpdfium operations.
pub type PdfResult<T> = std::result::Result<T, PdfError>;

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

    #[test]
    fn test_object_id_display() {
        let id = ObjectId {
            number: 42,
            generation: 0,
        };
        assert_eq!(format!("{id}"), "42 0 R");
    }

    #[test]
    fn test_object_id_equality() {
        let a = ObjectId {
            number: 1,
            generation: 0,
        };
        let b = ObjectId {
            number: 1,
            generation: 0,
        };
        let c = ObjectId {
            number: 1,
            generation: 1,
        };
        assert_eq!(a, b);
        assert_ne!(a, c);
    }

    #[test]
    fn test_pdf_error_from_parse_error() {
        let parse_err = ParseError::UnexpectedEof { offset: 100 };
        let pdf_err: PdfError = parse_err.into();
        assert!(matches!(pdf_err, PdfError::Parse(_)));
    }

    #[test]
    fn test_pdf_error_from_io_error() {
        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
        let pdf_err: PdfError = io_err.into();
        assert!(matches!(pdf_err, PdfError::Io(_)));
    }

    #[test]
    fn test_pdf_error_display() {
        let err = PdfError::CircularReference(ObjectId {
            number: 5,
            generation: 0,
        });
        assert_eq!(
            format!("{err}"),
            "circular reference detected at object 5 0 R"
        );
    }

    #[test]
    fn test_parse_error_display() {
        let err = ParseError::UnexpectedToken {
            offset: 42,
            expected: "integer".to_string(),
            found: "name".to_string(),
        };
        assert_eq!(
            format!("{err}"),
            "unexpected token at offset 42: expected integer, found name"
        );
    }

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

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