//! Error types preserved from the upstream public API.

use std::fmt;

use libremetaverse_types::compat::ExternalError;

use crate::Error;

macro_rules! message_error {
    ($name:ident, $fallback:literal) => {
        #[derive(Clone, Debug, Eq, PartialEq)]
        pub struct $name {
            message: String,
            inner: Option<ExternalError>,
        }

        impl $name {
            fn with_parts(message: String, inner: Option<ExternalError>) -> Self {
                Self { message, inner }
            }

            #[must_use]
            pub fn message(&self) -> &str {
                &self.message
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                if self.message.is_empty() {
                    formatter.write_str($fallback)
                } else {
                    formatter.write_str(&self.message)
                }
            }
        }

        impl std::error::Error for $name {
            fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
                self.inner
                    .as_ref()
                    .map(|error| error as &(dyn std::error::Error + 'static))
            }
        }
    };
}

message_error!(AppearanceManagerException, "appearance operation failed");
message_error!(MalformedDataException, "malformed packet data");
message_error!(RenderingException, "rendering failed");

impl AppearanceManagerException {
    pub fn new(message: String) -> Result<Self, Error> {
        Ok(Self::with_parts(message, None))
    }
}

impl MalformedDataException {
    pub fn new_with_constructor() -> Result<Self, Error> {
        Ok(Self::with_parts(String::new(), None))
    }

    pub fn new_with_string(message: String) -> Result<Self, Error> {
        Ok(Self::with_parts(message, None))
    }
}

impl RenderingException {
    pub fn new_with_string(message: String) -> Result<Self, Error> {
        Ok(Self::with_parts(message, None))
    }

    pub fn new_with_string_exception(
        message: String,
        inner_exception: ExternalError,
    ) -> Result<Self, Error> {
        Ok(Self::with_parts(message, Some(inner_exception)))
    }
}

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

    #[test]
    fn rendering_error_exposes_source() {
        let error = RenderingException::new_with_string_exception(
            "mesh failed".to_owned(),
            ExternalError("bad triangle".to_owned()),
        )
        .unwrap();
        assert_eq!(error.to_string(), "mesh failed");
        assert_eq!(
            std::error::Error::source(&error).unwrap().to_string(),
            "bad triangle"
        );
    }
}
