#[cfg(feature = "alloc")]
use alloc::boxed::Box;
use core::{convert::Infallible, fmt};
#[cfg(feature = "std")]
use std::io;

#[cfg(not(feature = "std"))]
use no_std_io2::io;

/// Alias for a Result with the error type [`Error`].
pub type Result<T> = core::result::Result<T, Error>;

#[derive(Debug)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum Error {
    Io(io::Error),
    #[cfg(feature = "alloc")]
    FromUtf8(alloc::string::FromUtf8Error),
    #[cfg(feature = "alloc")]
    Nul(alloc::ffi::NulError),
    TryFromInt(core::num::TryFromIntError),
    Borrow(core::cell::BorrowError),
    Discriminant,
    TagConvert,
    #[cfg(feature = "std")]
    Poison,
    Underrun {
        read_bits: u64,
        available_bits: u64,
    },
    EncodeSkipped,
    Magic(&'static [u8]),
    #[cfg(feature = "alloc")]
    /// A catch-all for errors generated by user code
    Boxed(Box<dyn core::error::Error + Send + Sync>),
    /// A catch-all for error generated by user code, with a static message
    Other(&'static str),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Io(e) => write!(f, "{e}"),
            #[cfg(feature = "alloc")]
            Self::FromUtf8(e) => write!(f, "{e}"),
            #[cfg(feature = "alloc")]
            Self::Nul(e) => write!(f, "{e}"),
            Self::TryFromInt(e) => write!(f, "{e}"),
            Self::Borrow(e) => write!(f, "{e}"),
            Self::Discriminant => write!(f, "unknown enum discriminant"),
            Self::TagConvert => write!(f, "failed to convert tag"),
            #[cfg(feature = "std")]
            Self::Poison => write!(f, "poisoned lock"),
            Self::Magic(expected) => write!(f, "magic mismatch. Expected: {expected:?}."),
            Self::Underrun {
                read_bits: read,
                available_bits: available,
            } => {
                write!(f, "buffer underrun: read {read} of {available} bits")
            }
            Self::EncodeSkipped => write!(f, "attempted to encode skipped enum variant"),
            #[cfg(feature = "alloc")]
            Self::Boxed(e) => write!(f, "{e}"),
            Self::Other(e) => write!(f, "other: {e}"),
        }
    }
}

impl From<io::Error> for Error {
    #[inline]
    fn from(value: io::Error) -> Self {
        Self::Io(value)
    }
}

#[cfg(feature = "alloc")]
impl From<alloc::string::FromUtf8Error> for Error {
    #[inline]
    fn from(value: alloc::string::FromUtf8Error) -> Self {
        Self::FromUtf8(value)
    }
}

#[cfg(feature = "alloc")]
impl From<alloc::ffi::NulError> for Error {
    #[inline]
    fn from(value: alloc::ffi::NulError) -> Self {
        Self::Nul(value)
    }
}

impl From<core::num::TryFromIntError> for Error {
    #[inline]
    fn from(value: core::num::TryFromIntError) -> Self {
        Self::TryFromInt(value)
    }
}

impl From<core::cell::BorrowError> for Error {
    #[inline]
    fn from(value: core::cell::BorrowError) -> Self {
        Self::Borrow(value)
    }
}

impl From<Infallible> for Error {
    #[inline]
    fn from(_: Infallible) -> Self {
        unreachable!()
    }
}

#[cfg(feature = "std")]
impl<T> From<std::sync::PoisonError<T>> for Error {
    fn from(_: std::sync::PoisonError<T>) -> Self {
        Self::Poison
    }
}

impl core::error::Error for Error {}

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

    #[allow(unused)]
    trait IsSizedSendSync: Sized + Send + Sync {}

    impl IsSizedSendSync for Error {}
}
