use sha3::TurboShake256;
use sha3::digest::{ExtendableOutput, Update, XofReader};
use zeroize::Zeroize;

use crate::Salt;

const PASSWORD_LEN: usize = 32;

/// Represents a salt used for password-based key derivation.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Zeroize)]
pub struct Password([u8; PASSWORD_LEN]);

impl Password {
    /// Represents the byte length.
    pub const LEN: usize = PASSWORD_LEN;

    /// Creates a new [Password].
    pub const fn new() -> Self {
        Self([0u8; Self::LEN])
    }

    /// Creates a new random [Password].
    pub fn create() -> Self {
        let mut salt = Password::new();
        rand::fill(salt.as_mut());
        salt
    }

    /// Hashes the password with a salt.
    pub fn hash(val: &[u8], salt: &Salt) -> Self {
        let mut password = Self::new();
        let mut hasher = TurboShake256::from_core(sha3::TurboShake256Core::new(2));

        hasher.update(salt.as_ref());
        hasher.update(val);

        let mut reader = hasher.finalize_xof();
        reader.read(password.as_mut());
        password
    }

    /// Converts a slice into a [Password].
    pub fn from_slice(val: &[u8]) -> Self {
        let mut salt = Password::new();
        let len = std::cmp::min(Self::LEN, val.len());
        salt.0[..len].copy_from_slice(&val[..len]);
        salt
    }

    /// Gets a reference to the inner slice.
    pub const fn as_slice(&self) -> &[u8] {
        &self.0
    }

    /// Gets the [Password] length.
    pub const fn len(&self) -> usize {
        Self::LEN
    }

    /// Gets whether the [Password] is all zeroes.
    pub fn is_empty(&self) -> bool {
        self.0 == [0u8; Self::LEN]
    }
}

impl Default for Password {
    fn default() -> Self {
        Self::new()
    }
}

impl AsRef<[u8]> for Password {
    fn as_ref(&self) -> &[u8] {
        self.0.as_ref()
    }
}

impl AsMut<[u8]> for Password {
    fn as_mut(&mut self) -> &mut [u8] {
        self.0.as_mut()
    }
}

impl From<&[u8]> for Password {
    fn from(val: &[u8]) -> Self {
        Self::from_slice(val)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const RAND_PASSWORDS: usize = 1024;

    #[test]
    fn test_password() {
        let password_in = b"test password";

        let mut passwords = Vec::with_capacity(RAND_PASSWORDS);
        let mut salts = Vec::with_capacity(RAND_PASSWORDS);

        (0..RAND_PASSWORDS).for_each(|_| {
            let salt = Salt::random();
            let password = Password::hash(password_in.as_ref(), &salt);

            assert!(password != Password::new());
            assert!(!password.is_empty());

            // check the same input creates the same resulting password hash
            assert_eq!(Password::hash(password_in.as_ref(), &salt), password);

            // check the random salt creates unique password hashes
            assert!(!salts.contains(&salt));
            assert!(!passwords.contains(&password));

            salts.push(salt);
            passwords.push(password);
        });
    }
}
