#[cfg(feature = "encryption")]
mod encryption;

#[cfg(feature = "compression")]
mod compression;

/// A list of encapsulatins for the protocol.
/// Their order is executed while writing, while the reverse while reading.
///
/// The wrappers can be build in order with the builder methods for example:
///
/// ```ignore
/// use pipenet::Packs;
///
/// let key = &[0u8; 32];
/// let _wrappers = Packs::default()
///     .compress()
///     .encrypt(key);
/// ```
#[derive(Default)]
pub struct Packs {
    items: Vec<Pack>,
}

impl Packs {
    pub fn is_empty(&self) -> bool {
        self.items.is_empty()
    }

    #[cfg(feature = "compression")]
    pub fn compress(mut self) -> Self {
        self.items
            .push(Pack::Compression(compression::Compression::Lz4));
        self
    }

    #[cfg(feature = "encryption")]
    pub fn encrypt(mut self, key: &[u8]) -> Self {
        use chacha20poly1305::{ChaCha20Poly1305, KeyInit};

        let c = ChaCha20Poly1305::new(key.into());
        self.items
            .push(Pack::Encryption(encryption::Encryption::Ccp(c)));
        self
    }
}

// Generic packer/unpacker functions.
#[allow(unused)]
pub(crate) trait PackUnpack {
    fn pack(&self, data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        Ok(data.to_vec())
    }

    fn unpack(&self, data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        Ok(data.to_vec())
    }
}

impl PackUnpack for Packs {
    fn pack(&self, data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        if data.is_empty() {
            return Ok(Vec::new());
        }
        // packing executes them in order
        let mut res = data.to_vec();
        for p in self.items.iter() {
            res = p.pack(&res)?
        }
        Ok(res)
    }

    fn unpack(&self, data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        if data.is_empty() {
            return Ok(Vec::new());
        }
        // unpacking executes them in reverse
        let mut res = data.to_vec();
        for p in self.items.iter().rev() {
            res = p.unpack(&res)?
        }
        Ok(res)
    }
}

// The type of encapsulation for the message body.
// This allows to unpack the message body in different situations.
// Currently supports encryption, todo later: compression...
// A stream can have multiple encapsulations and they will be applied in order
// when sending, but in reverse when receiving.
enum Pack {
    #[cfg(feature = "compression")]
    Compression(compression::Compression),
    #[cfg(feature = "encryption")]
    #[allow(unused)]
    Encryption(encryption::Encryption),
}

impl PackUnpack for Pack {
    #[cfg(any(feature = "encryption", feature = "compression"))]
    fn pack(&self, data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        match self {
            #[cfg(feature = "compression")]
            Pack::Compression(c) => c.pack(data),
            #[cfg(feature = "encryption")]
            Pack::Encryption(e) => e.pack(data),
        }
    }

    #[cfg(any(feature = "encryption", feature = "compression"))]
    fn unpack(&self, data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        match self {
            #[cfg(feature = "compression")]
            Pack::Compression(c) => c.unpack(data),
            #[cfg(feature = "encryption")]
            Pack::Encryption(e) => e.unpack(data),
        }
    }
}

#[cfg(all(feature = "compression", feature = "encryption"))]
#[cfg(test)]
mod test {
    use super::PackUnpack;
    use super::*;
    use chacha20poly1305::{ChaCha20Poly1305, KeyInit, aead::OsRng};
    use quickcheck::quickcheck;

    #[test]
    #[cfg(all(feature = "compression", feature = "encryption"))]
    fn check_combination() {
        fn check_array(data: Vec<u8>) -> bool {
            let k = ChaCha20Poly1305::generate_key(&mut OsRng);
            let p = Packs::default().compress().encrypt(&k);
            let packed = p.pack(&data).expect("cannot pack");
            let unpacked = p.unpack(&packed).expect("cannot unpack");
            data == unpacked
        }

        quickcheck(check_array as fn(Vec<u8>) -> bool);
    }

    #[test]
    #[cfg(all(feature = "compression", feature = "encryption"))]
    fn check_combination_reverse() {
        fn check_array(data: Vec<u8>) -> bool {
            let k = ChaCha20Poly1305::generate_key(&mut OsRng);
            let p = Packs::default().encrypt(&k).compress();
            let packed = p.pack(&data).expect("cannot pack");
            let unpacked = p.unpack(&packed).expect("cannot unpack");
            data == unpacked
        }

        quickcheck(check_array as fn(Vec<u8>) -> bool);
    }
}
