use std::error::Error;
use std::fmt::Debug;
use std::pin::Pin;
use std::task::{Context, Poll};

use bytes::Buf;
use http_body::{Body, Frame, SizeHint};
use proj::EitherProj;

/// Sum type with two cases: [`Left`] and [`Right`], used if a body can be one of
/// two distinct types.
///
/// [`Left`]: Either::Left
/// [`Right`]: Either::Right
#[derive(Debug, Clone, Copy)]
pub enum Either<L, R> {
    /// A value of type `L`
    Left(L),
    /// A value of type `R`
    Right(R),
}

impl<L, R> Either<L, R> {
    /// This function is part of the generated code from `pin-project-lite`,
    /// for a more in depth explanation and the rest of the generated code refer
    /// to the [`proj`] module.
    pub(crate) fn project(self: Pin<&mut Self>) -> EitherProj<L, R> {
        unsafe {
            match self.get_unchecked_mut() {
                Self::Left(left) => EitherProj::Left(Pin::new_unchecked(left)),
                Self::Right(right) => EitherProj::Right(Pin::new_unchecked(right)),
            }
        }
    }
}

impl<L> Either<L, L> {
    /// Convert [`Either`] into the inner type, if both `Left` and `Right` are
    /// of the same type.
    pub fn into_inner(self) -> L {
        match self {
            Either::Left(left) => left,
            Either::Right(right) => right,
        }
    }
}

impl<L, R, Data> Body for Either<L, R>
where
    L: Body<Data = Data>,
    R: Body<Data = Data>,
    L::Error: Into<Box<dyn Error + Send + Sync>>,
    R::Error: Into<Box<dyn Error + Send + Sync>>,
    Data: Buf,
{
    type Data = Data;
    type Error = Box<dyn Error + Send + Sync>;

    fn poll_frame(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
        match self.project() {
            EitherProj::Left(left) => left
                .poll_frame(cx)
                .map(|poll| poll.map(|opt| opt.map_err(Into::into))),
            EitherProj::Right(right) => right
                .poll_frame(cx)
                .map(|poll| poll.map(|opt| opt.map_err(Into::into))),
        }
    }

    fn is_end_stream(&self) -> bool {
        match self {
            Either::Left(left) => left.is_end_stream(),
            Either::Right(right) => right.is_end_stream(),
        }
    }

    fn size_hint(&self) -> SizeHint {
        match self {
            Either::Left(left) => left.size_hint(),
            Either::Right(right) => right.size_hint(),
        }
    }
}

pub(crate) mod proj {
    //! This code is the (cleaned output) generated by [pin-project-lite], as it
    //! does not support tuple variants.
    //!
    //! This is the altered expansion from the following snippet, expanded by
    //! `cargo-expand`:
    //!
    //! ```rust
    //! use pin_project_lite::pin_project;
    //!
    //! pin_project! {
    //!     #[project = EitherProj]
    //!     pub enum Either<L, R> {
    //!         Left {#[pin] left: L},
    //!         Right {#[pin] right: R}
    //!     }
    //! }
    //! ```
    //!
    //! [pin-project-lite]: https://docs.rs/pin-project-lite/latest/pin_project_lite/
    use std::marker::PhantomData;
    use std::pin::Pin;

    use super::Either;

    #[allow(dead_code)]
    #[allow(single_use_lifetimes)]
    #[allow(unknown_lints)]
    #[allow(clippy::mut_mut)]
    #[allow(clippy::redundant_pub_crate)]
    #[allow(clippy::ref_option_ref)]
    #[allow(clippy::type_repetition_in_bounds)]
    pub(crate) enum EitherProj<'__pin, L, R>
    where
        Either<L, R>: '__pin,
    {
        Left(Pin<&'__pin mut L>),
        Right(Pin<&'__pin mut R>),
    }

    #[allow(single_use_lifetimes)]
    #[allow(unknown_lints)]
    #[allow(clippy::used_underscore_binding)]
    #[allow(missing_debug_implementations)]
    const _: () = {
        #[allow(non_snake_case)]
        pub struct __Origin<'__pin, L, R> {
            __dummy_lifetime: PhantomData<&'__pin ()>,
            _Left: L,
            _Right: R,
        }
        impl<'__pin, L, R> Unpin for Either<L, R> where __Origin<'__pin, L, R>: Unpin {}

        trait MustNotImplDrop {}
        #[allow(drop_bounds)]
        impl<T: Drop> MustNotImplDrop for T {}
        impl<L, R> MustNotImplDrop for Either<L, R> {}
    };
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{BodyExt, Empty, Full};

    #[tokio::test]
    async fn data_left() {
        let full = Full::new(&b"hello"[..]);

        let mut value: Either<_, Empty<&[u8]>> = Either::Left(full);

        assert_eq!(value.size_hint().exact(), Some(b"hello".len() as u64));
        assert_eq!(
            value.frame().await.unwrap().unwrap().into_data().unwrap(),
            &b"hello"[..]
        );
        assert!(value.frame().await.is_none());
    }

    #[tokio::test]
    async fn data_right() {
        let full = Full::new(&b"hello!"[..]);

        let mut value: Either<Empty<&[u8]>, _> = Either::Right(full);

        assert_eq!(value.size_hint().exact(), Some(b"hello!".len() as u64));
        assert_eq!(
            value.frame().await.unwrap().unwrap().into_data().unwrap(),
            &b"hello!"[..]
        );
        assert!(value.frame().await.is_none());
    }

    #[test]
    fn into_inner() {
        let a = Either::<i32, i32>::Left(2);
        assert_eq!(a.into_inner(), 2)
    }
}
