use std::any::TypeId;

use paste::paste;
use seq_macro::seq;
use util_macros::sub;

pub trait Tuple: sealed::Sealed
{
    /// `Self` with the given type added as the last element.
    ///
    /// `(String, i32, u8)::WithElementAtEnd<Path> = (String, i32, u8, Path)`
    ///
    /// # Important note
    /// If `Self` has 16 elements, this will be `()`. The reason for this is that the
    /// `Tuple` trait is only implemented for tuples with up to and including 16 elements.
    type WithElementAtEnd<NewElem>: Tuple;

    /// `Self` without the last element.
    ///
    /// `(u16, AtomicU8)::WithoutLastElement = (u16,)`
    type WithoutLastElement: Tuple;

    /// The last element of `Self`.
    type LastElement;

    /// Self with all elements wrapped in [`Option`].
    type InOptions: Tuple;

    /// Pops the last element from this tuple, returning the new tuple and the popped
    /// element.
    fn pop_last(self) -> (Self::WithoutLastElement, Self::LastElement);

    /// Converts this tuple so that all elements are wrapped in [`Option`].
    fn into_in_options(self) -> Self::InOptions;
}

/// A tuple with element types that all have the lifetime `'static`.
pub trait WithAllElemLtStatic: Tuple + sealed::Sealed
{
    /// Returns the element at the given index.
    fn get_mut<Element: 'static>(&mut self, index: usize) -> Option<&mut Element>;
}

/// Using the type system, reduces the elements of a tuple to a single one. Each element
/// determines itself how it is handled.
pub trait Reduce<Operation>
{
    type Out;
}

pub trait ReduceElement<Accumulator, Operation>
{
    type Return;
}

macro_rules! tuple_reduce_elem_tuple {
    (overflow) => {
        ()
    };

    ($index: tt) => {
        paste! {
            [<Elem $index>]::Return
        }
    };
}

macro_rules! elem_type_by_index {
    (overflow) => {
        ()
    };

    ($index: tt) => {
        paste! {
            [<Elem $index>]
        }
    };
}

macro_rules! elem_by_index {
    (overflow) => {
        ()
    };

    ($index: tt, $self: ident) => {
        $self.$index
    };
}

macro_rules! all_except_last {
    (start $($rest: tt)*) => {
        all_except_last!(@[] $($rest)*)
    };

    (@[$($included_elem: ident,)*] $elem: ident $($rest: tt)+) => {
        all_except_last!(@[$($included_elem,)* $elem,] $($rest)*)
    };

    (@[$($included_elem: expr,)*] ($elem: expr) $($rest: tt)+) => {
        all_except_last!(@[$($included_elem,)* $elem,] $($rest)*)
    };

    (@[$($included_elem: ident,)*] $elem: ident) => {
        ($($included_elem,)*)
    };

    (@[$($included_elem: expr,)*] $elem: expr) => {
        ($($included_elem,)*)
    };

    (@[]) => {
        ()
    };
}

macro_rules! impl_tuple_traits {
    ($cnt: tt) => {
        seq!(I in 0..$cnt {
            impl<#(Elem~I,)*> Tuple for (#(Elem~I,)*)
            {
                type WithElementAtEnd<NewElem> = (#(Elem~I,)* NewElem,);

                type WithoutLastElement = all_except_last!(start #(Elem~I)*);

                type LastElement = sub!($cnt - 1, elem_type_by_index);

                type InOptions = (#(Option<Elem~I>,)*);

                fn pop_last(self) -> (Self::WithoutLastElement, Self::LastElement)
                {
                    (
                        all_except_last!(start #((self.I))*),
                        sub!($cnt - 1, elem_by_index, (self))
                    )
                }

                fn into_in_options(self) -> Self::InOptions
                {
                    #![allow(clippy::unused_unit)]
                    (#(Some(self.I),)*)
                }
            }

            impl<#(Elem~I: 'static,)*> WithAllElemLtStatic for (#(Elem~I,)*)
            {
                fn get_mut<Element: 'static>(&mut self, index: usize) -> Option<&mut Element>
                {
                    match index {
                        #(
                            I => {
                                assert!(TypeId::of::<Element>() == TypeId::of::<Elem~I>());

                                // SAFETY: It is checked above that the type is correct
                                Some(unsafe { &mut *(&raw mut self.I).cast::<Element>() })
                            }
                        )*
                        _ => None
                    }
                }
            }

            impl<#(Elem~I,)*> sealed::Sealed for (#(Elem~I,)*)
            {
            }

            paste! {
                impl<Operation, #(Elem~I,)*> Reduce<Operation> for (#(Elem~I,)*)
                where
                    #(
                        Elem~I: ReduceElement<
                            sub!(I - 1, tuple_reduce_elem_tuple), Operation
                        >,
                    )*
                {
                    type Out = sub!($cnt - 1, tuple_reduce_elem_tuple);
                }
            }
        });
    };
}

seq!(N in 0..16 {
    impl_tuple_traits!(N);
});

seq!(I in 0..16 {
    impl<#(Elem~I,)*> Tuple for (#(Elem~I,)*)
    {
        type WithElementAtEnd<NewElem> = ();

        type WithoutLastElement = all_except_last!(start #(Elem~I)*);

        type LastElement = Elem15;

        type InOptions = (#(Option<Elem~I>,)*);

        fn pop_last(self) -> (Self::WithoutLastElement, Self::LastElement)
        {
            (
                all_except_last!(start #((self.I))*),
                self.15
            )
        }

        fn into_in_options(self) -> Self::InOptions
        {
            #![allow(clippy::unused_unit)]
            (#(Some(self.I),)*)
        }
    }

    impl<#(Elem~I: 'static,)*> WithAllElemLtStatic for (#(Elem~I,)*)
    {
        fn get_mut<Element: 'static>(&mut self, index: usize) -> Option<&mut Element>
        {
            match index {
                #(
                    I => {
                        assert!(TypeId::of::<Element>() == TypeId::of::<Elem~I>());

                        // SAFETY: It is checked above that the type is correct
                        Some(unsafe { &mut *(&raw mut self.I).cast::<Element>() })
                    }
                )*
                _ => None
            }
        }
    }

    impl<#(Elem~I,)*> sealed::Sealed for (#(Elem~I,)*)
    {
    }
});

mod sealed
{
    pub trait Sealed {}
}