use {
    libc::c_int,
    pcre2_sys::{
        pcre2_get_error_message_8, PCRE2_ERROR_BADDATA, PCRE2_ERROR_NOMEMORY,
    },
};

/// A PCRE2 error.
///
/// An error can occur during compilation or during matching. The kind of this
/// error indicates the type of operation being performed when the error
/// occurred.
#[derive(Clone)]
pub struct Error {
    kind: ErrorKind,
    code: c_int,
    offset: Option<usize>,
}

/// The kind of an error indicates the type of operation that was attempted
/// that resulted in an error.
///
/// This enum may expand over time.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum ErrorKind {
    /// An error occurred during compilation of a regex.
    Compile,
    /// An error occurred during JIT compilation of a regex.
    JIT,
    /// An error occurred while matching.
    Match,
    /// An error occurred while querying a compiled regex for info.
    Info,
    /// An error occurred while setting an option.
    Option,
}

impl Error {
    /// Create a new compilation error.
    pub(crate) fn compile(code: c_int, offset: usize) -> Error {
        Error { kind: ErrorKind::Compile, code, offset: Some(offset) }
    }

    /// Create a new JIT compilation error.
    pub(crate) fn jit(code: c_int) -> Error {
        Error { kind: ErrorKind::JIT, code, offset: None }
    }

    /// Create a new matching error.
    pub(crate) fn matching(code: c_int) -> Error {
        Error { kind: ErrorKind::Match, code, offset: None }
    }

    /// Create a new info error.
    pub(crate) fn info(code: c_int) -> Error {
        Error { kind: ErrorKind::Info, code, offset: None }
    }

    /// Create a new option error.
    pub(crate) fn option(code: c_int) -> Error {
        Error { kind: ErrorKind::Option, code, offset: None }
    }

    /// Return the kind of this error.
    ///
    /// The kind indicates the type of operation that was attempted which
    /// resulted in this error.
    pub fn kind(&self) -> &ErrorKind {
        &self.kind
    }

    /// Return the raw underlying PCRE2 error code.
    ///
    /// This can be useful if one needs to determine exactly which error
    /// occurred, which can be done with case analysis over the constants
    /// exported in the `pcre2-sys` crate.
    pub fn code(&self) -> c_int {
        self.code
    }

    /// Return the underlying offset associated with this error, if one exists.
    ///
    /// The offset is typically only available for compile time errors, and
    /// is supposed to indicate the general position in the pattern where an
    /// error occurred.
    pub fn offset(&self) -> Option<usize> {
        self.offset
    }

    /// Returns the error message from PCRE2.
    pub fn error_message(&self) -> String {
        // PCRE2 docs say a buffer size of 120 bytes is enough, but we're
        // cautious and double it.
        let mut buf = [0u8; 240];
        let rc = unsafe {
            pcre2_get_error_message_8(self.code, buf.as_mut_ptr(), buf.len())
        };
        // Errors are only ever constructed from codes reported by PCRE2, so
        // our code should always be valid.
        assert!(rc != PCRE2_ERROR_BADDATA, "used an invalid error code");
        // PCRE2 docs claim 120 bytes is enough, and we use more, so...
        assert!(rc != PCRE2_ERROR_NOMEMORY, "buffer size too small");
        // Sanity check that we do indeed have a non-negative result. 0 is OK.
        assert!(rc >= 0, "expected non-negative but got {}", rc);
        String::from_utf8(buf[..rc as usize].to_vec()).expect("valid UTF-8")
    }
}

impl std::error::Error for Error {
    fn description(&self) -> &str {
        "pcre2 error"
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let msg = self.error_message();
        match self.kind {
            ErrorKind::Compile => match self.offset {
                None => {
                    write!(f, "PCRE2: error compiling pattern: {}", msg)
                }
                Some(offset) => {
                    write!(
                        f,
                        "PCRE2: error compiling pattern at offset {}: {}",
                        offset, msg
                    )
                }
            },
            ErrorKind::JIT => {
                write!(f, "PCRE2: error JIT compiling pattern: {}", msg)
            }
            ErrorKind::Match => {
                write!(f, "PCRE2: error matching: {}", msg)
            }
            ErrorKind::Info => {
                write!(f, "PCRE2: error getting info: {}", msg)
            }
            ErrorKind::Option => {
                write!(f, "PCRE2: error setting option: {}", msg)
            }
        }
    }
}

impl std::fmt::Debug for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        // We include the error message in the debug representation since
        // most humans probably don't have PCRE2 error codes memorized.
        f.debug_struct("Error")
            .field("kind", &self.kind)
            .field("code", &self.code)
            .field("offset", &self.offset)
            .field("message", &self.error_message())
            .finish()
    }
}
