use crate::{err::ValueTooBigError, *};
use arrayvec::ArrayVec;

/// The statically sized data at the start of an ICMPv6 packet (at least the first 8 bytes of an ICMPv6 packet).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Icmpv6Header {
    /// Type & type specific values & code.
    pub icmp_type: Icmpv6Type,
    /// Checksum in the ICMPv6 header.
    pub checksum: u16,
}

impl Icmpv6Header {
    /// Minimum number of bytes an ICMP header needs to have.
    ///
    /// Note that minimum size can be larger depending on
    /// the type and code.
    pub const MIN_LEN: usize = 8;

    /// Deprecated, use [`Icmpv6Header::MIN_LEN`] instead.
    #[deprecated(since = "0.14.0", note = "Please use Icmpv6Header::MIN_LEN instead")]
    pub const MIN_SERIALIZED_SIZE: usize = Icmpv6Header::MIN_LEN;

    /// Maximum number of bytes/octets an Icmpv6Header takes up
    /// in serialized form.
    ///
    /// Currently this number is determined by the biggest
    /// planned ICMPv6 header type, which is currently the
    /// "Neighbor Discovery Protocol" "Redirect" message.
    pub const MAX_LEN: usize = 8 + 16 + 16;

    /// Deprecated, use [`Icmpv6Header::MAX_LEN`] instead.
    #[deprecated(since = "0.14.0", note = "Please use Icmpv6Header::MAX_LEN instead")]
    pub const MAX_SERIALIZED_SIZE: usize = Icmpv6Header::MAX_LEN;

    /// Setups a new header with the checksum being set to 0.
    #[inline]
    pub fn new(icmp_type: Icmpv6Type) -> Icmpv6Header {
        Icmpv6Header {
            icmp_type,
            checksum: 0, // will be filled in later
        }
    }

    /// Creates a [`Icmpv6Header`] with a checksum calculated based
    /// on the given payload & ip addresses from the IPv6 header.
    pub fn with_checksum(
        icmp_type: Icmpv6Type,
        source_ip: [u8; 16],
        destination_ip: [u8; 16],
        payload: &[u8],
    ) -> Result<Icmpv6Header, ValueTooBigError<usize>> {
        let checksum = icmp_type.calc_checksum(source_ip, destination_ip, payload)?;
        Ok(Icmpv6Header {
            icmp_type,
            checksum,
        })
    }

    /// Reads an icmp6 header from a slice directly and returns a tuple
    /// containing the resulting header & unused part of the slice.
    #[inline]
    pub fn from_slice(slice: &[u8]) -> Result<(Icmpv6Header, &[u8]), err::LenError> {
        let header = Icmpv6Slice::from_slice(slice)?.header();
        let len = header.header_len();
        Ok((header, &slice[len..]))
    }

    /// Read a ICMPv6 header from the given reader
    #[cfg(feature = "std")]
    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
    pub fn read<T: std::io::Read + Sized>(reader: &mut T) -> Result<Icmpv6Header, std::io::Error> {
        // read the initial 8 bytes
        let mut start = [0u8; 8];
        reader.read_exact(&mut start)?;
        Ok(Icmpv6Slice { slice: &start }.header())
    }

    /// Write the ICMPv6 header to the given writer.
    #[cfg(feature = "std")]
    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
    pub fn write<T: std::io::Write + Sized>(&self, writer: &mut T) -> Result<(), std::io::Error> {
        writer.write_all(&self.to_bytes())
    }

    /// Serialized length of the header in bytes/octets.
    ///
    /// Note that this size is not the size of the entire
    /// ICMPv6 packet but only the header.
    #[inline]
    pub fn header_len(&self) -> usize {
        self.icmp_type.header_len()
    }

    /// If the ICMP type has a fixed size returns the number of
    /// bytes that should be present after the header of this type.
    #[inline]
    pub fn fixed_payload_size(&self) -> Option<usize> {
        self.icmp_type.fixed_payload_size()
    }

    /// Updates the checksum of the header.
    pub fn update_checksum(
        &mut self,
        source_ip: [u8; 16],
        destination_ip: [u8; 16],
        payload: &[u8],
    ) -> Result<(), ValueTooBigError<usize>> {
        self.checksum = self
            .icmp_type
            .calc_checksum(source_ip, destination_ip, payload)?;
        Ok(())
    }

    /// Returns the header on the wire bytes.
    #[inline]
    pub fn to_bytes(&self) -> ArrayVec<u8, { Icmpv6Header::MAX_LEN }> {
        let checksum_be = self.checksum.to_be_bytes();

        let return_trivial =
            |type_u8: u8, code_u8: u8| -> ArrayVec<u8, { Icmpv6Header::MAX_LEN }> {
                #[rustfmt::skip]
            let mut re = ArrayVec::from([
                type_u8, code_u8, checksum_be[0], checksum_be[1],
                0, 0, 0, 0,

                0, 0, 0, 0,
                0, 0, 0, 0,
                0, 0, 0, 0,
                0, 0, 0, 0,

                0, 0, 0, 0,
                0, 0, 0, 0,
                0, 0, 0, 0,
                0, 0, 0, 0,
            ]);
                // SAFETY: Safe as u8 has no destruction behavior and as 8 is smaller then 20.
                unsafe {
                    re.set_len(8);
                }
                re
            };

        let return_4u8 = |type_u8: u8,
                          code_u8: u8,
                          bytes5to8: [u8; 4]|
         -> ArrayVec<u8, { Icmpv6Header::MAX_LEN }> {
            #[rustfmt::skip]
            let mut re = ArrayVec::from([
                type_u8, code_u8, checksum_be[0], checksum_be[1],
                bytes5to8[0], bytes5to8[1], bytes5to8[2], bytes5to8[3],

                0, 0, 0, 0,
                0, 0, 0, 0,
                0, 0, 0, 0,
                0, 0, 0, 0,

                0, 0, 0, 0,
                0, 0, 0, 0,
                0, 0, 0, 0,
                0, 0, 0, 0,
            ]);
            // SAFETY: Safe as u8 has no destruction behavior and as 8 is smaller then 20.
            unsafe {
                re.set_len(8);
            }
            re
        };

        use crate::{icmpv6::*, Icmpv6Type::*};
        match self.icmp_type {
            Unknown {
                type_u8,
                code_u8,
                bytes5to8,
            } => return_4u8(type_u8, code_u8, bytes5to8),
            DestinationUnreachable(header) => return_trivial(TYPE_DST_UNREACH, header.code_u8()),
            PacketTooBig { mtu } => return_4u8(TYPE_PACKET_TOO_BIG, 0, mtu.to_be_bytes()),
            TimeExceeded(code) => return_trivial(TYPE_TIME_EXCEEDED, code.code_u8()),
            ParameterProblem(header) => return_4u8(
                TYPE_PARAMETER_PROBLEM,
                header.code.code_u8(),
                header.pointer.to_be_bytes(),
            ),
            EchoRequest(echo) => return_4u8(TYPE_ECHO_REQUEST, 0, echo.to_bytes()),
            EchoReply(echo) => return_4u8(TYPE_ECHO_REPLY, 0, echo.to_bytes()),
        }
    }
}

#[cfg(test)]
mod test {
    use crate::{
        err::{ValueTooBigError, ValueType},
        icmpv6::*,
        test_gens::*,
        *,
    };
    use alloc::{format, vec::Vec};
    use arrayvec::ArrayVec;
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn new(icmp_type in icmpv6_type_any()) {
            assert_eq!(
                Icmpv6Header::new(icmp_type.clone()),
                Icmpv6Header {
                    icmp_type,
                    checksum: 0,
                }
            );
        }
    }

    proptest! {
        #[cfg(not(any(target_pointer_width = "16", target_pointer_width = "32")))]
        #[test]
        fn with_checksum(
            ip_header in ipv6_any(),
            icmp_type in icmpv6_type_any(),
            // max length is u32::MAX - header_len (7)
            bad_len in (core::u32::MAX - 7) as usize..=(core::isize::MAX as usize),
            payload in proptest::collection::vec(any::<u8>(), 0..1024)
        ) {

            // error case
            {
                // SAFETY: In case the error is not triggered
                //         a segmentation fault will be triggered.
                let too_big_slice = unsafe {
                    //NOTE: The pointer must be initialized with a non null value
                    //      otherwise a key constraint of slices is not fulfilled
                    //      which can lead to crashes in release mode.
                    use core::ptr::NonNull;
                    core::slice::from_raw_parts(
                        NonNull::<u8>::dangling().as_ptr(),
                        bad_len
                    )
                };
                assert_eq!(
                    Icmpv6Header::with_checksum(icmp_type.clone(), ip_header.source, ip_header.destination, too_big_slice),
                    Err(ValueTooBigError{
                        actual: bad_len,
                        max_allowed: (core::u32::MAX - 8) as usize,
                        value_type: ValueType::Icmpv6PayloadLength,
                    })
                );
            }

            // non error case
            assert_eq!(
                Icmpv6Header::with_checksum(icmp_type.clone(), ip_header.source, ip_header.destination, &payload).unwrap(),
                Icmpv6Header {
                    icmp_type,
                    checksum: icmp_type.calc_checksum(ip_header.source, ip_header.destination, &payload).unwrap(),
                }
            );
        }
    }

    proptest! {
        #[test]
        fn from_slice(
            icmp_type in icmpv6_type_any(),
            checksum in any::<u16>(),
        ) {
            let bytes = {
                Icmpv6Header {
                    icmp_type: icmp_type.clone(),
                    checksum,
                }.to_bytes()
            };

            // ok case
            {
                let result = Icmpv6Header::from_slice(&bytes).unwrap();
                assert_eq!(
                    Icmpv6Header{
                        icmp_type,
                        checksum,
                    },
                    result.0,
                );
                assert_eq!(&bytes[8..], result.1);
            }


            // size error case
            for length in 0..8 {
                assert_eq!(
                    Icmpv6Header::from_slice(&bytes[..length]).unwrap_err(),
                    err::LenError{
                        required_len: bytes.len(),
                        len: length,
                        len_source: LenSource::Slice,
                        layer: err::Layer::Icmpv6,
                        layer_start_offset: 0
                    }
                );
            }
        }
    }

    proptest! {
        #[test]
        fn read(
            icmp_type in icmpv6_type_any(),
            checksum in any::<u16>(),
        ) {
            let header = Icmpv6Header {
                icmp_type: icmp_type.clone(),
                checksum,
            };
            let bytes = header.to_bytes();

            // ok case
            {
                let mut cursor = std::io::Cursor::new(&bytes);
                let result = Icmpv6Header::read(&mut cursor).unwrap();
                assert_eq!(header, result,);
                assert_eq!(header.header_len() as u64, cursor.position());
            }

            // size error case
            for length in 0..header.header_len() {
                let mut cursor = std::io::Cursor::new(&bytes[..length]);
                assert!(Icmpv6Header::read(&mut cursor).is_err());
            }
        }
    }

    proptest! {
        #[test]
        fn write(
            icmp_type in icmpv6_type_any(),
            checksum in any::<u16>(),
            bad_len in 0..8usize
        ) {
            // normal case
            {
                let mut buffer = Vec::with_capacity(icmp_type.header_len());
                let header = Icmpv6Header {
                    icmp_type,
                    checksum,
                };
                header.write(&mut buffer).unwrap();
                assert_eq!(
                    &header.to_bytes(),
                    &buffer[..]
                );
            }

            // error case
            {
                let mut buffer = [0u8;Icmpv6Header::MAX_LEN];
                let mut writer = std::io::Cursor::new(&mut buffer[..bad_len]);
                Icmpv6Header {
                    icmp_type,
                    checksum,
                }.write(&mut writer).unwrap_err();
            }
        }
    }

    proptest! {
        #[test]
        fn header_len(icmp_type in icmpv6_type_any(), checksum in any::<u16>()) {
            assert_eq!(
                icmp_type.header_len(),
                Icmpv6Header{
                    icmp_type,
                    checksum
                }.header_len()
            );
        }
    }

    proptest! {
        #[test]
        fn fixed_payload_size(icmp_type in icmpv6_type_any(), checksum in any::<u16>()) {
            assert_eq!(
                icmp_type.fixed_payload_size(),
                Icmpv6Header{
                    icmp_type,
                    checksum
                }.fixed_payload_size()
            );
        }
    }

    proptest! {
        #[test]
        #[cfg(not(any(target_pointer_width = "16", target_pointer_width = "32")))]
        fn update_checksum(
            ip_header in ipv6_any(),
            icmp_type in icmpv6_type_any(),
            start_checksum in any::<u16>(),
            // max length is u32::MAX - header_len (7)
            bad_len in (core::u32::MAX - 7) as usize..=(core::isize::MAX as usize),
            payload in proptest::collection::vec(any::<u8>(), 0..1024)
        ) {

            // error case
            {
                // SAFETY: In case the error is not triggered
                //         a segmentation fault will be triggered.
                let too_big_slice = unsafe {
                    //NOTE: The pointer must be initialized with a non null value
                    //      otherwise a key constraint of slices is not fulfilled
                    //      which can lead to crashes in release mode.
                    use core::ptr::NonNull;
                    core::slice::from_raw_parts(
                        NonNull::<u8>::dangling().as_ptr(),
                        bad_len
                    )
                };
                assert_eq!(
                    Icmpv6Header{
                        icmp_type,
                        checksum: 0
                    }.update_checksum(ip_header.source, ip_header.destination, too_big_slice),
                    Err(ValueTooBigError{
                        actual: bad_len,
                        max_allowed: (u32::MAX - 8) as usize,
                        value_type: ValueType::Icmpv6PayloadLength
                    })
                );
            }

            // normal case
            assert_eq!(
                {
                    let mut header = Icmpv6Header{
                        icmp_type,
                        checksum: start_checksum,
                    };
                    header.update_checksum(ip_header.source, ip_header.destination, &payload).unwrap();
                    header
                },
                Icmpv6Header{
                    icmp_type,
                    checksum: icmp_type.calc_checksum(ip_header.source, ip_header.destination, &payload).unwrap(),
                }
            );
        }
    }

    proptest! {
        #[test]
        fn to_bytes(
            checksum in any::<u16>(),
            rand_u32 in any::<u32>(),
            rand_4bytes in any::<[u8;4]>(),
        ) {
            use Icmpv6Type::*;

            let with_5to8_bytes = |type_u8: u8, code_u8: u8, bytes5to8: [u8;4]| -> ArrayVec<u8, { Icmpv6Header::MAX_LEN }> {
                let mut bytes = ArrayVec::<u8, { Icmpv6Header::MAX_LEN }>::new();
                bytes.push(type_u8);
                bytes.push(code_u8);
                bytes.try_extend_from_slice(&checksum.to_be_bytes()).unwrap();
                bytes.try_extend_from_slice(&bytes5to8).unwrap();
                bytes
            };

            let simple_bytes = |type_u8: u8, code_u8: u8| -> ArrayVec<u8, { Icmpv6Header::MAX_LEN }> {
                with_5to8_bytes(type_u8, code_u8, [0;4])
            };

            // destination unreachable
            for (code, code_u8) in dest_unreachable_code_test_consts::VALID_VALUES {
                assert_eq!(
                    Icmpv6Header{
                        icmp_type: DestinationUnreachable(code),
                        checksum
                    }.to_bytes(),
                    simple_bytes(TYPE_DST_UNREACH, code_u8)
                );
            }

            // packet too big
            assert_eq!(
                Icmpv6Header{
                    icmp_type: PacketTooBig{ mtu: rand_u32 },
                    checksum
                }.to_bytes(),
                with_5to8_bytes(TYPE_PACKET_TOO_BIG, 0, rand_u32.to_be_bytes())
            );

            // time exceeded
            for (code, code_u8) in time_exceeded_code_test_consts::VALID_VALUES {
                assert_eq!(
                    Icmpv6Header{
                        icmp_type: TimeExceeded(code),
                        checksum
                    }.to_bytes(),
                    simple_bytes(TYPE_TIME_EXCEEDED, code_u8)
                );
            }

            // parameter problem
            for (code, code_u8) in parameter_problem_code_test_consts::VALID_VALUES {
                assert_eq!(
                    Icmpv6Header{
                        icmp_type: ParameterProblem(
                            ParameterProblemHeader{
                                code,
                                pointer: rand_u32,
                            }
                        ),
                        checksum
                    }.to_bytes(),
                    with_5to8_bytes(TYPE_PARAMETER_PROBLEM, code_u8, rand_u32.to_be_bytes())
                );
            }

            // echo request
            assert_eq!(
                Icmpv6Header{
                    icmp_type: EchoRequest(IcmpEchoHeader {
                        id: u16::from_be_bytes([rand_4bytes[0], rand_4bytes[1]]),
                        seq: u16::from_be_bytes([rand_4bytes[2], rand_4bytes[3]]),
                    }),
                    checksum
                }.to_bytes(),
                with_5to8_bytes(TYPE_ECHO_REQUEST, 0, rand_4bytes)
            );

            // echo reply
            assert_eq!(
                Icmpv6Header{
                    icmp_type: EchoReply(IcmpEchoHeader {
                        id: u16::from_be_bytes([rand_4bytes[0], rand_4bytes[1]]),
                        seq: u16::from_be_bytes([rand_4bytes[2], rand_4bytes[3]]),
                    }),
                    checksum
                }.to_bytes(),
                with_5to8_bytes(TYPE_ECHO_REPLY, 0, rand_4bytes)
            );

            // unknown
            for type_u8 in 0..=u8::MAX {
                for code_u8 in 0..=u8::MAX {
                    assert_eq!(
                        Icmpv6Header{
                            icmp_type: Unknown {
                                type_u8,
                                code_u8,
                                bytes5to8: rand_4bytes,
                            },
                            checksum
                        }.to_bytes(),
                        with_5to8_bytes(type_u8, code_u8, rand_4bytes)
                    );
                }
            }
        }
    }

    #[test]
    fn debug() {
        let t = Icmpv6Type::Unknown {
            type_u8: 0,
            code_u8: 1,
            bytes5to8: [2, 3, 4, 5],
        };
        assert_eq!(
            format!(
                "{:?}",
                Icmpv6Header {
                    icmp_type: t.clone(),
                    checksum: 7
                }
            ),
            format!("Icmpv6Header {{ icmp_type: {:?}, checksum: {:?} }}", t, 7)
        );
    }

    proptest! {
        #[test]
        fn clone_eq(icmp_type in icmpv6_type_any(), checksum in any::<u16>()) {
            let header = Icmpv6Header{ icmp_type, checksum };
            assert_eq!(header, header.clone());
        }
    }
}
