use super::super::*;

/// IPv6 fragment header.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Ipv6FragmentHeader {
    /// IP protocol number specifying the next header or transport layer protocol.
    ///
    /// See [IpNumber] or [ip_number] for a definition of the known values.
    pub next_header: IpNumber,
    /// Offset of the current IP payload relative to the start of the fragmented
    /// packet payload.
    pub fragment_offset: IpFragOffset,
    /// True if more fragment packets will follow. False if this is the last packet.
    pub more_fragments: bool,
    /// Identifcation value generated by the source.
    pub identification: u32,
}

impl Ipv6FragmentHeader {
    /// Length of the serialized header.
    pub const LEN: usize = 8;

    /// Create a new fragmentation header with the given parameters.
    ///
    /// Note that the `fragment_offset` can only support values between 0 and 0x1fff (inclusive).
    pub const fn new(
        next_header: IpNumber,
        fragment_offset: IpFragOffset,
        more_fragments: bool,
        identification: u32,
    ) -> Ipv6FragmentHeader {
        Ipv6FragmentHeader {
            next_header,
            fragment_offset,
            more_fragments,
            identification,
        }
    }

    /// Read an Ipv6FragmentHeader from a slice and return the header & unused parts of the slice.
    pub fn from_slice(slice: &[u8]) -> Result<(Ipv6FragmentHeader, &[u8]), err::LenError> {
        let s = Ipv6FragmentHeaderSlice::from_slice(slice)?;
        let rest = &slice[8..];
        let header = s.to_header();
        Ok((header, rest))
    }

    /// Read an fragment header from the current reader position.
    #[cfg(feature = "std")]
    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
    pub fn read<T: std::io::Read + std::io::Seek + Sized>(
        reader: &mut T,
    ) -> Result<Ipv6FragmentHeader, std::io::Error> {
        let buffer = {
            let mut buffer: [u8; 8] = [0; 8];
            reader.read_exact(&mut buffer)?;
            buffer
        };

        Ok(Ipv6FragmentHeader {
            next_header: IpNumber(buffer[0]),
            fragment_offset: unsafe {
                // SAFE as the resulting number is guaranteed to have at most
                // 13 bits.
                IpFragOffset::new_unchecked(u16::from_be_bytes([
                    (buffer[2] >> 3) & 0b0001_1111u8,
                    ((buffer[2] << 5) & 0b1110_0000u8) | (buffer[3] & 0b0001_1111u8),
                ]))
            },
            more_fragments: 0 != buffer[3] & 0b1000_0000u8,
            identification: u32::from_be_bytes([buffer[4], buffer[5], buffer[6], buffer[7]]),
        })
    }

    /// Read an fragment header from the current reader position.
    #[cfg(feature = "std")]
    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
    pub fn read_limited<T: std::io::Read + std::io::Seek + Sized>(
        reader: &mut crate::io::LimitedReader<T>,
    ) -> Result<Ipv6FragmentHeader, crate::err::io::LimitedReadError> {
        use err::Layer;

        // set layer so errors contain the correct layer & offset
        reader.start_layer(Layer::Ipv6FragHeader);

        let buffer = {
            let mut buffer: [u8; 8] = [0; 8];
            reader.read_exact(&mut buffer)?;
            buffer
        };

        Ok(Ipv6FragmentHeader {
            next_header: IpNumber(buffer[0]),
            fragment_offset: unsafe {
                // SAFE as the resulting number is guaranteed to have at most
                // 13 bits.
                IpFragOffset::new_unchecked(u16::from_be_bytes([
                    (buffer[2] >> 3) & 0b0001_1111u8,
                    ((buffer[2] << 5) & 0b1110_0000u8) | (buffer[3] & 0b0001_1111u8),
                ]))
            },
            more_fragments: 0 != buffer[3] & 0b1000_0000u8,
            identification: u32::from_be_bytes([buffer[4], buffer[5], buffer[6], buffer[7]]),
        })
    }

    /// Writes a given IPv6 fragment header to the current position.
    #[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())
    }

    /// Length of the header in bytes.
    #[inline]
    pub fn header_len(&self) -> usize {
        Ipv6FragmentHeader::LEN
    }

    /// Checks if the fragment header actually fragments the packet.
    ///
    /// Returns false if the fragment offset is 0 and the more flag
    /// is not set. Otherwise returns true.
    ///
    /// [RFC8200](https://datatracker.ietf.org/doc/html/rfc8200) explicitly
    /// states that fragment headers that don't fragment the packet payload are
    /// allowed. See the following quote from
    /// RFC8200 page 32:
    ///
    /// > Revised the text to handle the case of fragments that are whole
    /// > datagrams (i.e., both the Fragment Offset field and the M flag
    /// > are zero).  If received, they should be processed as a
    /// > reassembled packet.  Any other fragments that match should be
    /// > processed independently.  The Fragment creation process was
    /// > modified to not create whole datagram fragments (Fragment
    /// > Offset field and the M flag are zero).  See
    /// > [RFC6946](https://datatracker.ietf.org/doc/html/6946) and
    /// > [RFC8021](https://datatracker.ietf.org/doc/html/rfc8021) for more
    /// > information."
    ///
    /// ```
    /// use etherparse::{Ipv6FragmentHeader, ip_number::UDP};
    ///
    /// // offset 0 & no more fragments result in an unfragmented payload
    /// {
    ///     let header = Ipv6FragmentHeader::new(UDP, 0.try_into().unwrap(), false, 123);
    ///     assert!(false == header.is_fragmenting_payload());
    /// }
    ///
    /// // offset 0 & but more fragments will come -> fragmented
    /// {
    ///     let header = Ipv6FragmentHeader::new(UDP, 0.try_into().unwrap(), true, 123);
    ///     assert!(header.is_fragmenting_payload());
    /// }
    ///
    /// // offset non zero & no more fragments will come -> fragmented
    /// {
    ///     let header = Ipv6FragmentHeader::new(UDP, 1.try_into().unwrap(), false, 123);
    ///     assert!(header.is_fragmenting_payload());
    /// }
    /// ```
    #[inline]
    pub fn is_fragmenting_payload(&self) -> bool {
        self.more_fragments || (0 != self.fragment_offset.value())
    }

    /// Returns the serialized form of the header as a statically
    /// sized byte array.
    #[inline]
    pub fn to_bytes(&self) -> [u8; 8] {
        let fo_be: [u8; 2] = self.fragment_offset.value().to_be_bytes();
        let id_be = self.identification.to_be_bytes();
        [
            self.next_header.0,
            0,
            (((fo_be[0] << 3) & 0b1111_1000u8) | ((fo_be[1] >> 5) & 0b0000_0111u8)),
            ((fo_be[1] & 0b0001_1111u8)
                | if self.more_fragments {
                    0b1000_0000u8
                } else {
                    0
                }),
            id_be[0],
            id_be[1],
            id_be[2],
            id_be[3],
        ]
    }
}

#[cfg(test)]
mod test {
    use crate::{test_gens::*, *};
    use alloc::{format, vec::Vec};
    use proptest::prelude::*;
    use std::io::Cursor;

    proptest! {
        #[test]
        fn debug(input in ipv6_fragment_any()) {
            assert_eq!(
                &format!(
                    "Ipv6FragmentHeader {{ next_header: {:?}, fragment_offset: {:?}, more_fragments: {}, identification: {} }}",
                    input.next_header,
                    input.fragment_offset,
                    input.more_fragments,
                    input.identification
                ),
                &format!("{:?}", input)
            );
        }
    }

    proptest! {
        #[test]
        fn clone_eq(input in ipv6_fragment_any()) {
            assert_eq!(input, input.clone());
        }
    }

    proptest! {
        #[test]
        fn new(
            next_header in ip_number_any(),
            fragment_offset in 0..IpFragOffset::MAX_U16,
            more_fragments in any::<bool>(),
            identification in any::<u32>(),
        ) {
            let a = Ipv6FragmentHeader::new(
                next_header,
                fragment_offset.try_into().unwrap(),
                more_fragments,
                identification
            );
            assert_eq!(next_header, a.next_header);
            assert_eq!(fragment_offset, a.fragment_offset.value());
            assert_eq!(more_fragments, a.more_fragments);
            assert_eq!(identification, a.identification);
        }
    }

    proptest! {
        #[test]
        fn from_slice(
            input in ipv6_fragment_any(),
            dummy_data in proptest::collection::vec(any::<u8>(), 0..20)
        ) {
            // serialize
            let mut buffer: Vec<u8> = Vec::with_capacity(8 + dummy_data.len());
            input.write(&mut buffer).unwrap();
            buffer.extend(&dummy_data[..]);

            // calls with a valid result
            {
                let (result, rest) = Ipv6FragmentHeader::from_slice(&buffer[..]).unwrap();
                assert_eq!(input, result);
                assert_eq!(&buffer[8..], rest);
            }
            // call with not enough data in the slice
            for len in 0..Ipv6FragmentHeader::LEN {
                assert_eq!(
                    Ipv6FragmentHeader::from_slice(&buffer[0..len]).unwrap_err(),
                    err::LenError{
                        required_len: Ipv6FragmentHeader::LEN,
                        len: len,
                        len_source: LenSource::Slice,
                        layer: err::Layer::Ipv6FragHeader,
                        layer_start_offset: 0,
                    }
                );
            }
        }
    }

    proptest! {
        #[test]
        fn read(
            input in ipv6_fragment_any(),
            dummy_data in proptest::collection::vec(any::<u8>(), 0..20)
        ) {
            use std::io::ErrorKind;

            // serialize
            let mut buffer: Vec<u8> = Vec::with_capacity(8 + dummy_data.len());
            input.write(&mut buffer).unwrap();
            buffer.extend(&dummy_data[..]);

            // calls with a valid result
            {
                let mut cursor = Cursor::new(&buffer);
                let result = Ipv6FragmentHeader::read(&mut cursor).unwrap();
                assert_eq!(input, result);
                assert_eq!(cursor.position(), 8);
            }

            // call with not enough data in the slice
            for len in 0..Ipv6FragmentHeader::LEN {
                let mut cursor = Cursor::new(&buffer[0..len]);
                assert_eq!(
                    Ipv6FragmentHeader::read(&mut cursor)
                    .unwrap_err()
                    .kind(),
                    ErrorKind::UnexpectedEof
                );
            }
        }
    }

    proptest! {
        #[test]
        fn write(input in ipv6_fragment_any()) {

            // normal write
            {
                let mut buffer = Vec::with_capacity(8);
                input.write(&mut buffer).unwrap();
                assert_eq!(
                    &buffer,
                    &input.to_bytes()
                );
            }

            // not enough memory for write
            for len in 0..Ipv6FragmentHeader::LEN {
                let mut buffer = [0u8;Ipv6FragmentHeader::LEN];
                let mut cursor = Cursor::new(&mut buffer[..len]);
                assert!(
                    input.write(&mut cursor).is_err()
                );
            }
        }
    }

    proptest! {
        #[test]
        fn header_len(input in ipv6_fragment_any()) {
            assert_eq!(8, input.header_len());
        }
    }

    proptest! {
        #[test]
        fn is_fragmenting_payload(
            non_zero_offset in 1u16..0b0001_1111_1111_1111u16,
            identification in any::<u32>(),
            next_header in ip_number_any(),

        ) {
            // negative case
            {
                let header = Ipv6FragmentHeader {
                    next_header,
                    fragment_offset: 0.try_into().unwrap(),
                    more_fragments: false,
                    identification
                };
                assert!(false == header.is_fragmenting_payload());
            }
            // positive case (non zero offset)
            {
                let header = Ipv6FragmentHeader {
                    next_header,
                    fragment_offset: non_zero_offset.try_into().unwrap(),
                    more_fragments: false,
                    identification
                };
                assert!(header.is_fragmenting_payload());
            }

            // positive case (more fragments)
            {
                let header = Ipv6FragmentHeader {
                    next_header,
                    fragment_offset: 0.try_into().unwrap(),
                    more_fragments: true,
                    identification
                };
                assert!(header.is_fragmenting_payload());
            }

            // positive case (non zero offset & more fragments)
            {
                let header = Ipv6FragmentHeader {
                    next_header,
                    fragment_offset: non_zero_offset.try_into().unwrap(),
                    more_fragments: true,
                    identification
                };
                assert!(header.is_fragmenting_payload());
            }
        }
    }

    proptest! {
        #[test]
        fn to_bytes(input in ipv6_fragment_any()) {

            // normal write
            {
                let fragment_offset_be = input.fragment_offset.value().to_be_bytes();
                let id_be = input.identification.to_be_bytes();
                assert_eq!(
                    &input.to_bytes(),
                    &[
                        input.next_header.0,
                        0,
                        (
                            (fragment_offset_be[0] << 3 & 0b1111_1000u8) |
                            (fragment_offset_be[1] >> 5 & 0b0000_0111u8)
                        ),
                        (
                            (fragment_offset_be[1] & 0b0001_1111u8) |
                            if input.more_fragments {
                                0b1000_0000u8
                            } else {
                                0u8
                            }
                        ),
                        id_be[0],
                        id_be[1],
                        id_be[2],
                        id_be[3],
                    ]
                );
            }
        }
    }
}
