use milter_sys as sys;
use std::{
    error,
    ffi::{CString, NulError},
    fmt::{self, Display, Formatter},
    num::TryFromIntError,
    result,
    str::Utf8Error,
};

/// A result type specialised for milter errors.
pub type Result<T> = result::Result<T, Error>;

/// Various kinds of errors that can occur in a milter application.
#[derive(Debug)]
pub enum Error {
    /// The socket specification provided to the `Milter` could not be used.
    SocketConfig(CString),
    /// The timeout duration configuration parameter of the `Milter` is invalid.
    TimeoutConfig(i32),
    /// The socket backlog length configuration parameter of the `Milter` is
    /// invalid.
    SocketBacklogConfig(i32),
    /// Registration of the `Milter`’s configuration with the milter library
    /// failed.
    Registration,
    /// The milter library’s event loop (`main` function) exited with a failure
    /// status.
    Main,
    /// The milter library’s event loop was shut down after a panic in a
    /// callback.
    CallbackPanic,
    /// A milter library function returned the failure status code.
    FailureStatus,
    /// Conversion to a C-style string at the FFI boundary (Rust/C) failed, with
    /// cause attached.
    FfiCString(NulError),
    /// Conversion to an integer type at the FFI boundary (Rust/C) failed, with
    /// cause attached.
    FfiInt(TryFromIntError),
    /// Conversion to a UTF-8 string at the FFI boundary (Rust/C) failed, with
    /// cause attached.
    FfiUtf8(Utf8Error),
    /// An unspecific error variant, with cause attached.
    ///
    /// # Examples
    ///
    /// This variant is provided as a convenience; use it to propagate arbitrary
    /// errors out of a callback.
    ///
    /// ```no_run
    /// use milter::{Context, Error, Status};
    /// use std::fs::File;
    ///
    /// fn handle_data(_: Context<()>) -> milter::Result<Status> {
    ///     let _ = File::create("/tmp/archive").map_err(|e| Error::Custom(e.into()))?;
    ///
    ///     Ok(Status::Continue)
    /// }
    /// ```
    Custom(Box<dyn error::Error + Send + Sync>),
}

impl Display for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        use Error::*;

        match self {
            SocketConfig(socket) => write!(f, "could not use socket {:?}", socket),
            TimeoutConfig(secs) => write!(f, "invalid timeout duration: {} seconds", secs),
            SocketBacklogConfig(len) => write!(f, "invalid socket backlog length: {}", len),
            Registration => write!(f, "failed to register configuration with milter library"),
            Main => write!(f, "milter library main returned failure status"),
            CallbackPanic => write!(f, "panic in callback"),
            FailureStatus => write!(f, "milter library returned failure status"),
            FfiCString(_) => write!(f, "failed to convert string at FFI boundary"),
            FfiInt(_) => write!(f, "failed to convert integer at FFI boundary"),
            FfiUtf8(_) => write!(f, "failed to read UTF-8 string at FFI boundary"),
            Custom(source) => source.fmt(f),
        }
    }
}

impl error::Error for Error {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        use Error::*;

        match self {
            FfiCString(source) => Some(source),
            FfiInt(source) => Some(source),
            FfiUtf8(source) => Some(source),
            Custom(source) => Some(source.as_ref()),
            _ => None,
        }
    }
}

impl From<NulError> for Error {
    fn from(error: NulError) -> Self {
        Self::FfiCString(error)
    }
}

impl From<TryFromIntError> for Error {
    fn from(error: TryFromIntError) -> Self {
        Self::FfiInt(error)
    }
}

impl From<Utf8Error> for Error {
    fn from(error: Utf8Error) -> Self {
        Self::FfiUtf8(error)
    }
}

impl From<Box<dyn error::Error + Send + Sync + 'static>> for Error {
    fn from(error: Box<dyn error::Error + Send + Sync + 'static>) -> Self {
        Self::Custom(error)
    }
}

impl From<String> for Error {
    fn from(error: String) -> Self {
        Self::Custom(error.into())
    }
}

impl From<&str> for Error {
    fn from(error: &str) -> Self {
        Self::Custom(error.into())
    }
}

/// Result status code returned from calls to milter library functions.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) enum ReturnCode {
    Success,
    Failure,
}

impl From<i32> for ReturnCode {
    fn from(ret_code: i32) -> Self {
        match ret_code {
            sys::MI_SUCCESS => Self::Success,
            _ => Self::Failure,
        }
    }
}

impl From<ReturnCode> for Result<()> {
    fn from(ret_code: ReturnCode) -> Self {
        match ret_code {
            ReturnCode::Success => Ok(()),
            ReturnCode::Failure => Err(Error::FailureStatus),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::error::Error as _;

    #[test]
    fn no_source_error() {
        let error = Error::FailureStatus;

        assert!(error.source().is_none());
    }

    #[test]
    fn downcast_some_source_error() {
        let nul_error = CString::new("test\0").unwrap_err();
        let error = Error::FfiCString(nul_error);

        let source = error.source().unwrap().downcast_ref::<NulError>().unwrap();

        assert_eq!(source.nul_position(), 4);
    }
}
