// SPDX-License-Identifier: MIT

#[macro_export(local_inner_macros)]
macro_rules! getter {
    ($buffer: ident, $name:ident, slice, $offset:expr) => {
        impl<'a, T: AsRef<[u8]> + ?Sized> $buffer<&'a T> {
            pub fn $name(&self) -> &'a [u8] {
                &self.buffer.as_ref()[$offset]
            }
        }
    };
    ($buffer: ident, $name:ident, $ty:tt, $offset:expr) => {
        impl<'a, T: AsRef<[u8]>> $buffer<T> {
            getter!($name, $ty, $offset);
        }
    };
    ($name:ident, u8, $offset:expr) => {
        pub fn $name(&self) -> u8 {
            self.buffer.as_ref()[$offset]
        }
    };
    ($name:ident, u16, $offset:expr) => {
        pub fn $name(&self) -> u16 {
            use $crate::byteorder::{ByteOrder, NativeEndian};
            NativeEndian::read_u16(&self.buffer.as_ref()[$offset])
        }
    };
    ($name:ident, u32, $offset:expr) => {
        pub fn $name(&self) -> u32 {
            use $crate::byteorder::{ByteOrder, NativeEndian};
            NativeEndian::read_u32(&self.buffer.as_ref()[$offset])
        }
    };
    ($name:ident, u64, $offset:expr) => {
        pub fn $name(&self) -> u64 {
            use $crate::byteorder::{ByteOrder, NativeEndian};
            NativeEndian::read_u64(&self.buffer.as_ref()[$offset])
        }
    };
    ($name:ident, i8, $offset:expr) => {
        pub fn $name(&self) -> i8 {
            self.buffer.as_ref()[$offset]
        }
    };
    ($name:ident, i16, $offset:expr) => {
        pub fn $name(&self) -> i16 {
            use $crate::byteorder::{ByteOrder, NativeEndian};
            NativeEndian::read_i16(&self.buffer.as_ref()[$offset])
        }
    };
    ($name:ident, i32, $offset:expr) => {
        pub fn $name(&self) -> i32 {
            use $crate::byteorder::{ByteOrder, NativeEndian};
            NativeEndian::read_i32(&self.buffer.as_ref()[$offset])
        }
    };
    ($name:ident, i64, $offset:expr) => {
        pub fn $name(&self) -> i64 {
            use $crate::byteorder::{ByteOrder, NativeEndian};
            NativeEndian::read_i64(&self.buffer.as_ref()[$offset])
        }
    };
}

#[macro_export(local_inner_macros)]
macro_rules! setter {
    ($buffer: ident, $name:ident, slice, $offset:expr) => {
        impl<'a, T: AsRef<[u8]> + AsMut<[u8]> + ?Sized> $buffer<&'a mut T> {
            $crate::paste::item! {
                pub fn [<$name _mut>](&mut self) -> &mut [u8] {
                    &mut self.buffer.as_mut()[$offset]
                }
            }
        }
    };
    ($buffer: ident, $name:ident, $ty:tt, $offset:expr) => {
        impl<'a, T: AsRef<[u8]> + AsMut<[u8]>> $buffer<T> {
            setter!($name, $ty, $offset);
        }
    };
    ($name:ident, u8, $offset:expr) => {
        $crate::paste::item! {
            pub fn [<set_ $name>](&mut self, value: u8) {
                self.buffer.as_mut()[$offset] = value;
            }
        }
    };
    ($name:ident, u16, $offset:expr) => {
        $crate::paste::item! {
            pub fn [<set_ $name>](&mut self, value: u16) {
                use $crate::byteorder::{ByteOrder, NativeEndian};
                NativeEndian::write_u16(&mut self.buffer.as_mut()[$offset], value)
            }
        }
    };
    ($name:ident, u32, $offset:expr) => {
        $crate::paste::item! {
            pub fn [<set_ $name>](&mut self, value: u32) {
                use $crate::byteorder::{ByteOrder, NativeEndian};
                NativeEndian::write_u32(&mut self.buffer.as_mut()[$offset], value)
            }
        }
    };
    ($name:ident, u64, $offset:expr) => {
        $crate::paste::item! {
            pub fn [<set_ $name>](&mut self, value: u64) {
                use $crate::byteorder::{ByteOrder, NativeEndian};
                NativeEndian::write_u64(&mut self.buffer.as_mut()[$offset], value)
            }
        }
    };
    ($name:ident, i8, $offset:expr) => {
        $crate::paste::item! {
            pub fn [<set_ $name>](&mut self, value: i8) {
                self.buffer.as_mut()[$offset] = value;
            }
        }
    };
    ($name:ident, i16, $offset:expr) => {
        $crate::paste::item! {
            pub fn [<set_ $name>](&mut self, value: i16) {
                use $crate::byteorder::{ByteOrder, NativeEndian};
                NativeEndian::write_i16(&mut self.buffer.as_mut()[$offset], value)
            }
        }
    };
    ($name:ident, i32, $offset:expr) => {
        $crate::paste::item! {
            pub fn [<set_ $name>](&mut self, value: i32) {
                use $crate::byteorder::{ByteOrder, NativeEndian};
                NativeEndian::write_i32(&mut self.buffer.as_mut()[$offset], value)
            }
        }
    };
    ($name:ident, i64, $offset:expr) => {
        $crate::paste::item! {
            pub fn [<set_ $name>](&mut self, value: i64) {
                use $crate::byteorder::{ByteOrder, NativeEndian};
                NativeEndian::write_i64(&mut self.buffer.as_mut()[$offset], value)
            }
        }
    };
}

#[macro_export(local_inner_macros)]
macro_rules! buffer {
    ($name:ident($buffer_len:expr) { $($field:ident : ($ty:tt, $offset:expr)),* $(,)? }) => {
        buffer!($name { $($field: ($ty, $offset),)* });
        buffer_check_length!($name($buffer_len));
    };

    ($name:ident { $($field:ident : ($ty:tt, $offset:expr)),* $(,)? }) => {
        buffer_common!($name);
        fields!($name {
            $($field: ($ty, $offset),)*
        });
    };

    ($name:ident, $buffer_len:expr) => {
        buffer_common!($name);
        buffer_check_length!($name($buffer_len));
    };

    ($name:ident) => {
        buffer_common!($name);
    };
}

#[macro_export(local_inner_macros)]
macro_rules! fields {
    ($buffer:ident { $($name:ident : ($ty:tt, $offset:expr)),* $(,)? }) => {
        $(
            getter!($buffer, $name, $ty, $offset);
        )*

            $(
                setter!($buffer, $name, $ty, $offset);
            )*
    }
}

#[macro_export]
macro_rules! buffer_check_length {
    ($name:ident($buffer_len:expr)) => {
        impl<T: AsRef<[u8]>> $name<T> {
            pub fn new_checked(buffer: T) -> Result<Self, DecodeError> {
                let packet = Self::new(buffer);
                packet.check_buffer_length()?;
                Ok(packet)
            }

            fn check_buffer_length(&self) -> Result<(), DecodeError> {
                let len = self.buffer.as_ref().len();
                if len < $buffer_len {
                    Err(format!(
                        concat!(
                            "invalid ",
                            stringify!($name),
                            ": length {} < {}"
                        ),
                        len, $buffer_len
                    )
                    .into())
                } else {
                    Ok(())
                }
            }
        }
    };
}

#[macro_export]
macro_rules! buffer_common {
    ($name:ident) => {
        #[derive(Debug, PartialEq, Eq, Clone, Copy)]
        pub struct $name<T> {
            buffer: T,
        }

        impl<T: AsRef<[u8]>> $name<T> {
            pub fn new(buffer: T) -> Self {
                Self { buffer }
            }

            pub fn into_inner(self) -> T {
                self.buffer
            }
        }

        impl<'a, T: AsRef<[u8]> + ?Sized> $name<&'a T> {
            pub fn inner(&self) -> &'a [u8] {
                &self.buffer.as_ref()[..]
            }
        }

        impl<'a, T: AsRef<[u8]> + AsMut<[u8]> + ?Sized> $name<&'a mut T> {
            pub fn inner_mut(&mut self) -> &mut [u8] {
                &mut self.buffer.as_mut()[..]
            }
        }
    };
}
