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

const DB_KEY_LEN: usize = 16;

/// Represents a key used for database entries.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Zeroize)]
pub struct DbKey([u8; DB_KEY_LEN]);

impl DbKey {
    /// Represents the byte length.
    pub const LEN: usize = DB_KEY_LEN;

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

    /// Hashes input [`u64`] for use as a database key.
    pub fn from_u64(val: u64) -> Self {
        Self::from_slice(val.to_le_bytes().as_ref())
    }

    /// Hashes input slice for use as a database key.
    pub fn from_slice(key: &[u8]) -> Self {
        let mut out = Self::new();
        let mut hasher = TurboShake256::from_core(sha3::TurboShake256Core::new(1));
        hasher.update(key);
        let mut reader = hasher.finalize_xof();
        reader.read(out.as_mut());
        out
    }

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

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

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

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

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

impl<'d> From<&'d DbKey> for &'d [u8] {
    fn from(val: &'d DbKey) -> Self {
        val.as_slice()
    }
}

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

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

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

    #[test]
    fn test_db_key() {
        let key_u64: u64 = 0xfeed_deaf;
        let key_bytes = key_u64.to_le_bytes();

        let db_key = DbKey::from_u64(key_u64);

        assert_eq!(DbKey::from_slice(key_bytes.as_ref()), db_key);

        assert!(db_key != DbKey::new());
        assert_eq!(db_key.len(), DbKey::LEN);
        assert!(!db_key.is_empty());
    }
}
