use crate::error::ptr::{MutPtr, OwnedPtr, SharedPtr};
use crate::error::{ConcreteError, DynError, Error, ErrorExt, OutOfMemory};
use core::{any::TypeId, fmt, ptr::NonNull};
use std_alloc::boxed::Box;

/// A vtable containing the `ErrorExt` methods for some type `T`.
///
/// This is used to create thin-pointer equivalents of `Box<dyn ErrorExt>`,
/// `&dyn ErrorExt`, and `&mut ErrorExt`, which would all otherwise be two words
/// in size.
///
/// # Safety
///
/// The safety contract for all vtable functions is the same:
///
/// * `SharedPtr<'_, DynError>`s must be valid for reading a `ConcreteError<T>`,
///   `MutPtr<'_, DynError>`s must additionally be valid for writing a
///   `ConcreteError<T>`, and `OwnedPtr<DynError>`s must additionally be valid
///   to deallocate with `ConcreteError<T>`'s layout.
///
/// * If a `OomOrDynError{Ref,Mut}` return value contains a `{Shared,Mut}Ptr<'_,
///   DynError>`, it must be valid for reading (and, in the case of `MutPtr`,
///   writing) `DynError`s.
#[repr(C)]
pub(crate) struct Vtable {
    pub(crate) display: unsafe fn(SharedPtr<'_, DynError>, &mut fmt::Formatter<'_>) -> fmt::Result,
    pub(crate) debug: unsafe fn(SharedPtr<'_, DynError>, &mut fmt::Formatter<'_>) -> fmt::Result,
    pub(crate) source: unsafe fn(SharedPtr<'_, DynError>) -> Option<&Error>,
    pub(crate) source_mut: unsafe fn(MutPtr<'_, DynError>) -> Option<&mut Error>,
    pub(crate) is: unsafe fn(SharedPtr<'_, DynError>, TypeId) -> bool,
    pub(crate) as_dyn_core_error:
        unsafe fn(SharedPtr<'_, DynError>) -> &(dyn core::error::Error + Send + Sync + 'static),
    pub(crate) into_boxed_dyn_core_error:
        unsafe fn(
            OwnedPtr<DynError>,
        )
            -> Result<Box<dyn core::error::Error + Send + Sync + 'static>, OutOfMemory>,
    pub(crate) drop_and_deallocate: unsafe fn(OwnedPtr<DynError>),

    /// Additional safety requirement: the `NonNull<u8>` pointer must be valid
    /// for writing a `T`.
    ///
    /// Upon successful return, a `T` will have been written to that memory
    /// block.
    pub(crate) move_into: unsafe fn(OwnedPtr<DynError>, NonNull<u8>),

    /// Conversion into `anyhow::Error` from `Box<Self>`.
    #[cfg(feature = "anyhow")]
    pub(crate) into_anyhow: unsafe fn(OwnedPtr<DynError>) -> anyhow::Error,
}

impl Vtable {
    /// Get the `Vtable` of the `E: ErrorExt` type parameter.
    pub(crate) fn of<E>() -> &'static Self
    where
        E: ErrorExt,
    {
        &Vtable {
            display: display::<E>,
            debug: debug::<E>,
            source: source::<E>,
            source_mut: source_mut::<E>,
            is: is::<E>,
            as_dyn_core_error: as_dyn_core_error::<E>,
            into_boxed_dyn_core_error: into_boxed_dyn_core_error::<E>,
            drop_and_deallocate: drop_and_deallocate::<E>,
            move_into: move_into::<E>,
            #[cfg(feature = "anyhow")]
            into_anyhow: into_anyhow::<E>,
        }
    }
}

unsafe fn display<E>(error: SharedPtr<'_, DynError>, f: &mut fmt::Formatter<'_>) -> fmt::Result
where
    E: ErrorExt,
{
    let error = error.cast::<ConcreteError<E>>();
    // Safety: implied by all vtable functions' safety contract.
    let error = unsafe { error.as_ref() };
    fmt::Display::fmt(error.error.ext_as_dyn_core_error(), f)
}

unsafe fn debug<E>(error: SharedPtr<'_, DynError>, f: &mut fmt::Formatter<'_>) -> fmt::Result
where
    E: ErrorExt,
{
    let error = error.cast::<ConcreteError<E>>();
    // Safety: implied by all vtable functions' safety contract.
    let error = unsafe { error.as_ref() };
    fmt::Debug::fmt(error.error.ext_as_dyn_core_error(), f)
}

unsafe fn source<E>(error: SharedPtr<'_, DynError>) -> Option<&Error>
where
    E: ErrorExt,
{
    let error = error.cast::<ConcreteError<E>>();
    // Safety: implied by all vtable functions' safety contract.
    let error = unsafe { error.as_ref() };
    error.error.ext_source()
}

unsafe fn source_mut<E>(error: MutPtr<'_, DynError>) -> Option<&mut Error>
where
    E: ErrorExt,
{
    let mut error = error.cast::<ConcreteError<E>>();
    // Safety: implied by all vtable functions' safety contract.
    let error = unsafe { error.as_mut() };
    error.error.ext_source_mut()
}

unsafe fn is<E>(error: SharedPtr<'_, DynError>, type_id: TypeId) -> bool
where
    E: ErrorExt,
{
    let error = error.cast::<ConcreteError<E>>();
    // Safety: implied by all vtable functions' safety contract.
    let error = unsafe { error.as_ref() };
    error.error.ext_is(type_id)
}

unsafe fn as_dyn_core_error<E>(
    error: SharedPtr<'_, DynError>,
) -> &(dyn core::error::Error + Send + Sync + 'static)
where
    E: ErrorExt,
{
    let error = error.cast::<ConcreteError<E>>();
    // Safety: implied by all vtable functions' safety contract.
    let error = unsafe { error.as_ref() };
    error.error.ext_as_dyn_core_error()
}

unsafe fn into_boxed_dyn_core_error<E>(
    error: OwnedPtr<DynError>,
) -> Result<Box<dyn core::error::Error + Send + Sync + 'static>, OutOfMemory>
where
    E: ErrorExt,
{
    let error = error.cast::<ConcreteError<E>>();
    // Safety: implied by all vtable functions' safety contract.
    let error = unsafe { error.into_box() };
    error.error.ext_into_boxed_dyn_core_error()
}

unsafe fn drop_and_deallocate<E>(error: OwnedPtr<DynError>)
where
    E: ErrorExt,
{
    let error = error.cast::<ConcreteError<E>>();
    // Safety: implied by all vtable functions' safety contract.
    let _ = unsafe { error.into_box() };
}

unsafe fn move_into<E>(error: OwnedPtr<DynError>, ret_ptr: NonNull<u8>)
where
    E: ErrorExt,
{
    let error = error.cast::<ConcreteError<E>>();
    // Safety: implied by all vtable functions' safety contract.
    let error = unsafe { error.into_box() };
    // Safety: Implied by `move`'s additional safety safety requirement.
    unsafe {
        error.error.ext_move(ret_ptr);
    }
}

#[cfg(feature = "anyhow")]
unsafe fn into_anyhow<E>(error: OwnedPtr<DynError>) -> anyhow::Error
where
    E: ErrorExt,
{
    let error = error.cast::<ConcreteError<E>>();
    // Safety: implied by all vtable functions' safety contract.
    let error = unsafe { error.into_box() };
    error.error.ext_into_anyhow()
}
