/*-
 * Scram-rs - a SCRAM authentification authorization library
 * 
 * Copyright (C) 2021  Aleksandr Morozov
 * Copyright (C) 2025 Aleksandr Morozov
 * 
 * The syslog-rs crate can be redistributed and/or modified
 * under the terms of either of the following licenses:
 *
 *   1. the Mozilla Public License Version 2.0 (the “MPL”) OR
 *
 *   2. The MIT License (MIT)
 *                     
 *   3. EUROPEAN UNION PUBLIC LICENCE v. 1.2 EUPL © the European Union 2007, 2016
 */


use core::fmt;
#[cfg(feature = "std")]
use std::num::NonZeroU32;
#[cfg(not(feature = "std"))]
use core::num::NonZeroU32;

#[cfg(not(feature = "std"))]
use alloc::vec::Vec;

use crate::scram_error;
use crate::ScramErrorCode;
use crate::ScramServerError;

use super::scram_error::ScramResult;

/// A trait which implements different operations which required by the 
/// SCRAM algorithm.
pub trait ScramOps
{
    /// A function which performs the XOR of two arrays
    #[cfg(not(feature = "xor_without_u128"))]
    fn xor_arrays(a: &[u8], b: &[u8]) -> ScramResult<Vec<u8>>
    {
        if a.len() != b.len()
        {
            scram_error!(
                ScramErrorCode::InternalError,
                ScramServerError::OtherError,
                "xor arrays size mismatch: a: '{}', b: '{}'", a.len(), b.len()
            );
        }

        let mut ret = Vec::<u8>::with_capacity(a.len());
        let len = a.len();
        let mut i = 0;

        while i < len
        {
            let leni = len-i;

            if leni >= (u128::BITS/8) as usize && ((&a[i]) as *const _ as *const u128).is_aligned() == true
            {
                let a1 = u128::from_be_bytes(a[i..i+(u128::BITS/8) as usize].try_into().unwrap());
                let b1 = u128::from_be_bytes(b[i..i+(u128::BITS/8) as usize].try_into().unwrap());

                let c1 = a1 ^ b1;

                ret.extend_from_slice(&c1.to_be_bytes());

                i += (u128::BITS/8) as usize;
            }
            else if leni >= (u64::BITS/8) as usize
            {
                let a1 = u64::from_be_bytes(a[i..i+(u64::BITS/8) as usize].try_into().unwrap());
                let b1 = u64::from_be_bytes(b[i..i+(u64::BITS/8) as usize].try_into().unwrap());

                let c1 = a1 ^ b1;

                ret.extend_from_slice(&c1.to_be_bytes());

                i += (u64::BITS/8) as usize;
            }
            else if leni >= (u32::BITS/8) as usize
            {
                let a1 = u32::from_be_bytes(a[i..i+(u32::BITS/8) as usize].try_into().unwrap());
                let b1 = u32::from_be_bytes(b[i..i+(u32::BITS/8) as usize].try_into().unwrap());

                let c1 = a1 ^ b1;

                ret.extend_from_slice(&c1.to_be_bytes());

                i += (u32::BITS/8) as usize;
            }
            else if leni >= (u16::BITS/8) as usize 
            {
                let a1 = u16::from_be_bytes(a[i..i+(u16::BITS/8) as usize].try_into().unwrap());
                let b1 = u16::from_be_bytes(b[i..i+(u16::BITS/8) as usize].try_into().unwrap());

                let c1 = a1 ^ b1;

                ret.extend_from_slice(&c1.to_be_bytes());

                i += (u16::BITS/8) as usize;
            }
            else 
            {
                ret.push(a[i] ^ b[i]);

                i += 1;
            }   
        }
        
        return Ok(ret);
    }

    #[cfg(feature = "xor_without_u128")]
    fn xor_arrays(a: &[u8], b: &[u8]) -> ScramResult<Vec<u8>>
    {
        if a.len() != b.len()
        {
            scram_error!(
                ScramErrorCode::InternalError,
                ScramServerError::OtherError,
                "xor arrays size mismatch: a: '{}', b: '{}'", a.len(), b.len()
            );
        }

        let mut ret = Vec::<u8>::with_capacity(a.len());
        let len = a.len();
        let mut i = 0;

        while i < len
        {
            let leni = len-i;

            if leni >= (u64::BITS/8) as usize
            {
                let a1 = u64::from_be_bytes(a[i..i+(u64::BITS/8) as usize].try_into().unwrap());
                let b1 = u64::from_be_bytes(b[i..i+(u64::BITS/8) as usize].try_into().unwrap());

                let c1 = a1 ^ b1;

                ret.extend_from_slice(&c1.to_be_bytes());

                i += (u64::BITS/8) as usize;
            }
            else if leni >= (u32::BITS/8) as usize
            {
                let a1 = u32::from_be_bytes(a[i..i+(u32::BITS/8) as usize].try_into().unwrap());
                let b1 = u32::from_be_bytes(b[i..i+(u32::BITS/8) as usize].try_into().unwrap());

                let c1 = a1 ^ b1;

                ret.extend_from_slice(&c1.to_be_bytes());

                i += (u32::BITS/8) as usize;
            }
            else if leni >= (u16::BITS/8) as usize 
            {
                let a1 = u16::from_be_bytes(a[i..i+(u16::BITS/8) as usize].try_into().unwrap());
                let b1 = u16::from_be_bytes(b[i..i+(u16::BITS/8) as usize].try_into().unwrap());

                let c1 = a1 ^ b1;

                ret.extend_from_slice(&c1.to_be_bytes());

                i += (u16::BITS/8) as usize;
            }
            else 
            {
                ret.push(a[i] ^ b[i]);

                i += 1;
            }   
        }
        
        return Ok(ret);
    }
}

/// A reference function which works 100%. Can be used for testing against.
#[inline]
pub
fn xor_arrays_reference(a: &[u8], b: &[u8]) -> ScramResult<Vec<u8>>
{
    if a.len() != b.len()
    {
        scram_error!(
            ScramErrorCode::InternalError,
            ScramServerError::OtherError,
            "xor arrays size mismatch: a: '{}', b: '{}'", a.len(), b.len()
        );
    }

    let ret = a.into_iter().zip(b).map(|(a, b)| a ^ b).collect::<Vec<u8>>();
    return Ok(ret);
}

/// A standart interface for cryptographic operations.
/// 
/// None of the functions verifies (where appliciable) an amount of iterations i.e
/// it must be non-zero, but the recommended amount is not forced.
pub trait ScramHashing: ScramOps + fmt::Debug
{
    /// A minimum recommended iteration rounds for `derive` operation.
    const MIN_ITERS: NonZeroU32;

    /// A function which hashes the data using the hash function.
    fn hash(data: &[u8]) -> Vec<u8>;

    /// A function which performs an HMAC using the hash function.
    fn hmac(data: &[u8], key: &[u8]) -> ScramResult<Vec<u8>>;

    /// A function which does PBKDF2 key derivation using the hash function.
    fn derive(password: &[u8], salt: &[u8], iterations: NonZeroU32) -> ScramResult<Vec<u8>>;
}

/// All hashing code dedicated for SHA1. Both rust native and ring inplemetations.
pub use super::scram_hashing_sha1::*;

/// All hasing code dedicated for SHA256. Both rust native and ring inplemetations.
pub use super::scram_hashing_sha2::*;

/// All hashing code dedicated for SHA512. Both rust native and ring inplemetations.
pub use super::scram_hashing_sha5::*;


#[cfg(feature = "std")]
#[cfg(test)]
mod tests
{
    use std::time::Instant;

    use crate::{xor_arrays_reference, ScramCommon, ScramOps};

    fn perf_xor(a: &[u8], b: &[u8])
    {
        let s = Instant::now();

        let res_ref = xor_arrays_reference(a, b).unwrap();

        let e1 = s.elapsed();
        
        struct XorTest; impl ScramOps for XorTest {}
        
        let s = Instant::now();

        let xor_res = XorTest::xor_arrays(a, b).unwrap();

        let e2 = s.elapsed();

        println!("ref: {:?}, fast: {:?}", e1, e2);


        assert_eq!(xor_res.as_slice(), res_ref.as_slice());
    }

    #[test]
    fn test_xor()
    {
        for _ in 0..20
        {
            let a = ScramCommon::sc_random(128).unwrap();
            let b = ScramCommon::sc_random(128).unwrap();
            perf_xor(&a, &b);
        }
    }

    #[test]
    fn test_xor2()
    {
        for _ in 0..20
        {
            let a = ScramCommon::sc_random(1).unwrap();
            let b = ScramCommon::sc_random(1).unwrap();
            perf_xor(&a, &b);
        }
    }
}
