// SPDX-License-Identifier: MIT

/// This macro provides the same functionality as `core::mem::offset_of`,
/// except that only one level of field access is supported.  The declaration
/// of the struct must be wrapped with `with_offsets! { }`.
///
/// It is needed because `offset_of!` was only stabilized in Rust 1.77.
#[cfg(not(has_offset_of))]
#[macro_export]
macro_rules! offset_of {
    ($Container:ty, $field:ident) => {
        <$Container>::OFFSET_TO__.$field
    };
}

/// A wrapper for struct declarations, that allows using `offset_of!` in
/// versions of Rust prior to 1.77
#[macro_export]
macro_rules! with_offsets {
    // This method to generate field offset constants comes from:
    //
    //     https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=10a22a9b8393abd7b541d8fc844bc0df
    //
    // used under MIT license with permission of Yandros aka Daniel Henry-Mantilla
    (
        $(#[$struct_meta:meta])*
        $struct_vis:vis
        struct $StructName:ident {
            $(
                $(#[$field_meta:meta])*
                $field_vis:vis
                $field_name:ident : $field_ty:ty
            ),*
            $(,)?
        }
    ) => (
        #[cfg(not(has_offset_of))]
        const _: () = {
            struct StructOffsetsHelper<T>(std::marker::PhantomData<T>);
            const END_OF_PREV_FIELD: usize = 0;

            // populate StructOffsetsHelper<T> with associated consts,
            // one for each field
            $crate::with_offsets! {
                @struct $StructName
                @names [ $($field_name)* ]
                @tys [ $($field_ty ,)*]
            }

            // now turn StructOffsetsHelper<T>'s consts into a single struct,
            // applying field visibility.  This provides better error messages
            // than if offset_of! used StructOffsetsHelper::<T> directly.
            pub
            struct StructOffsets {
                $(
                    $field_vis
                    $field_name: usize,
                )*
            }
            impl $StructName {
                pub
                const OFFSET_TO__: StructOffsets = StructOffsets {
                    $(
                        $field_name: StructOffsetsHelper::<$StructName>::$field_name,
                    )*
                };
            }
        };
    );

    (
        @struct $StructName:ident
        @names []
        @tys []
    ) => ();

    (
        @struct $StructName:ident
        @names [$field_name:ident $($other_names:tt)*]
        @tys [$field_ty:ty , $($other_tys:tt)*]
    ) => (
        #[allow(non_local_definitions)]
        #[allow(clippy::modulo_one)]
        impl StructOffsetsHelper<$StructName> {
            #[allow(nonstandard_style)]
            const $field_name: usize = {
                const ALIGN: usize = std::mem::align_of::<$field_ty>();
                const TRAIL: usize = END_OF_PREV_FIELD % ALIGN;
                END_OF_PREV_FIELD + (if TRAIL == 0 { 0usize } else { ALIGN - TRAIL })
            };
        }
        const _: () = {
            const END_OF_PREV_FIELD: usize =
                StructOffsetsHelper::<$StructName>::$field_name +
                std::mem::size_of::<$field_ty>()
            ;
            $crate::with_offsets! {
                @struct $StructName
                @names [$($other_names)*]
                @tys [$($other_tys)*]
            }
        };
    );
}

#[cfg(test)]
mod tests {
    use crate::offset_of;

    #[repr(C)]
    struct Foo {
        a: u16,
        b: u32,
        c: u64,
        d: u16,
    }

    #[repr(C)]
    struct Bar {
        pub a: u16,
        pub b: u64,
        c: Foo,
        d: u64,
    }

    crate::with_offsets! {
        #[repr(C)]
        struct Bar {
            pub a: u16,
            pub b: u64,
            c: Foo,
            d: u64,
        }
    }

    #[repr(C)]
    pub struct Baz {
        b: u32,
        a: u8,
    }
    crate::with_offsets! {
        #[repr(C)]
        pub struct Baz {
            b: u32,
            a: u8,
        }
    }

    #[test]
    fn test_offset_of() {
        const OFFSET_TO_C: usize = offset_of!(Bar, c);

        assert_eq!(offset_of!(Bar, a), 0);
        assert_eq!(offset_of!(Bar, b), 8);
        assert_eq!(OFFSET_TO_C, 16);
        assert_eq!(offset_of!(Bar, d), 40);

        assert_eq!(offset_of!(Baz, b), 0);
        assert_eq!(offset_of!(Baz, a), 4);
    }
}