use crate::chaum_pedersen::{ChaumPedersen, GroupParams};
use crate::conversion::ByteConvertible;
use crate::rand::RandomGenerator;
use num_bigint::{BigUint, RandBigInt};
use num_traits::One;
use rand::rngs::OsRng;
use std::error::Error;
#[derive(Clone)]
pub struct DiscreteLogChaumPedersen {}
impl ChaumPedersen for DiscreteLogChaumPedersen {
    type Secret = BigUint;
    type CommitmentRandom = BigUint;
    type Response = BigUint;
    type Challenge = BigUint;
    type GroupParameters = GroupParams<BigUint>;
    type CommitParameters = (BigUint, BigUint, BigUint, BigUint);
    fn commitment(
        params: &Self::GroupParameters, x: &Self::Secret,
    ) -> (Self::CommitParameters, Self::CommitmentRandom)
    where
        Self: Sized,
    {
        let y1 = params.g.modpow(x, ¶ms.p);
        let y2 = params.h.modpow(x, ¶ms.p);
        let mut rng = OsRng;
        let k = rng.gen_biguint_below(¶ms.p);
        let r1 = params.g.modpow(&k, ¶ms.p);
        let r2 = params.h.modpow(&k, ¶ms.p);
        ((y1, y2, r1, r2), k)
    }
    fn challenge(params: &GroupParams<BigUint>) -> BigUint {
        let mut rng = OsRng;
        rng.gen_biguint_below(¶ms.p)
    }
    fn challenge_response(
        params: &Self::GroupParameters, k: &Self::CommitmentRandom, c: &Self::Challenge,
        x: &Self::Secret,
    ) -> Self::Response
    where
        Self: Sized,
    {
        if k >= &(c * x) {
            (k - c * x).modpow(&BigUint::one(), ¶ms.q)
        } else {
            ¶ms.q - (c * x - k).modpow(&BigUint::one(), ¶ms.q)
        }
    }
    fn verify(
        params: &Self::GroupParameters, s: &Self::Response, c: &Self::Challenge,
        cp: &Self::CommitParameters,
    ) -> bool {
        let (y1, y2, r1, r2) = cp;
        let lhs1 = params.g.modpow(s, ¶ms.p);
        let rhs1 = (r1 * y1.modpow(&(¶ms.p - c - BigUint::one()), ¶ms.p)) % ¶ms.p;
        let lhs2 = params.h.modpow(s, ¶ms.p);
        let rhs2 = (r2 * y2.modpow(&(¶ms.p - c - BigUint::one()), ¶ms.p)) % ¶ms.p;
        lhs1 == rhs1 && lhs2 == rhs2
    }
}
impl ByteConvertible<BigUint> for BigUint {
    fn convert_to(t: &BigUint) -> Vec<u8> {
        t.to_bytes_be()
    }
    fn convert_from(bytes: &[u8]) -> Result<BigUint, Box<dyn Error>> {
        Ok(BigUint::from_bytes_be(bytes))
    }
}
impl RandomGenerator<BigUint> for BigUint {
    fn generate_random() -> Result<BigUint, Box<dyn std::error::Error>> {
        use rand::RngCore;
        let mut rng = OsRng;
        let mut bytes = [0u8; 32];
        rng.fill_bytes(&mut bytes);
        Ok(BigUint::from_bytes_be(&bytes))
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::chaum_pedersen::constants::{
        RFC5114_MODP_1024_160_BIT_PARAMS, RFC5114_MODP_2048_224_BIT_PARAMS,
        RFC5114_MODP_2048_256_BIT_PARAMS,
    };
    use crate::chaum_pedersen::test::test_execute_protocol;
    use crate::rand::RandomGenerator;
    use num_bigint::ToBigUint;
    #[test]
    fn biguint_conversion_round_trip() {
        let original = 123456789u64.to_biguint().unwrap();
        let bytes = BigUint::convert_to(&original);
        let recovered = BigUint::convert_from(&bytes).unwrap();
        assert_eq!(original, recovered);
    }
    #[test]
    fn test_discrete_log_commitment() {
        let g = BigUint::from(4u32);
        let h = BigUint::from(9u32);
        let p = BigUint::from(23u32);
        let q = BigUint::from(11u32);
        let x = BigUint::from(3u32);
        let params = GroupParams::<BigUint> {
            g: g.clone(),
            h: h.clone(),
            p: p.clone(),
            q: q.clone(),
        };
        let (cp, _k) = DiscreteLogChaumPedersen::commitment(¶ms, &x);
        let (y1, y2, r1, r2) = cp;
        assert_eq!(y1, params.g.modpow(&x, ¶ms.p));
        assert_eq!(y2, params.h.modpow(&x, ¶ms.p));
        assert!(r1 < params.p && r2 < params.p);
    }
    #[test]
    fn test_discrete_log_verification() {
        let g = BigUint::from(4u32);
        let h = BigUint::from(9u32);
        let p = BigUint::from(23u32);
        let q = BigUint::from(11u32);
        let x = BigUint::from(3u32);
        let params = GroupParams::<BigUint> {
            g: g.clone(),
            h: h.clone(),
            p: p.clone(),
            q: q.clone(),
        };
        assert!(test_execute_protocol::<DiscreteLogChaumPedersen>(¶ms, &x));
    }
    #[test]
    fn test_rfc_1024_160_bits_params() {
        let params = RFC5114_MODP_1024_160_BIT_PARAMS.to_owned();
        let mut rng = OsRng;
        let x = rng.gen_biguint_below(¶ms.p);
        assert!(test_execute_protocol::<DiscreteLogChaumPedersen>(¶ms, &x));
    }
    #[test]
    fn test_rfc_2048_224_bits_params() {
        let params = RFC5114_MODP_2048_224_BIT_PARAMS.to_owned();
        let mut rng = OsRng;
        let x = rng.gen_biguint_below(¶ms.p);
        assert!(test_execute_protocol::<DiscreteLogChaumPedersen>(¶ms, &x));
    }
    #[test]
    fn test_rfc_2048_256_bits_params() {
        let params = RFC5114_MODP_2048_256_BIT_PARAMS.to_owned();
        let mut rng = OsRng;
        let x = rng.gen_biguint_below(¶ms.p);
        assert!(test_execute_protocol::<DiscreteLogChaumPedersen>(¶ms, &x));
    }
    #[test]
    fn test_fail_rfc_1024_160_bits_params() {
        let params = RFC5114_MODP_1024_160_BIT_PARAMS.to_owned();
        let mut rng = OsRng;
        let x = rng.gen_biguint_below(¶ms.p);
        let (cp, _) = DiscreteLogChaumPedersen::commitment(¶ms, &x);
        let c = DiscreteLogChaumPedersen::challenge(¶ms);
        let fake_response = BigUint::generate_random().unwrap();
        let verified = DiscreteLogChaumPedersen::verify(¶ms, &fake_response, &c, &cp);
        assert!(!verified);
    }
    #[test]
    fn test_fail_rfc_2048_224_bits_params() {
        let params = RFC5114_MODP_2048_224_BIT_PARAMS.to_owned();
        let mut rng = OsRng;
        let x = rng.gen_biguint_below(¶ms.p);
        let (cp, _) = DiscreteLogChaumPedersen::commitment(¶ms, &x);
        let c = DiscreteLogChaumPedersen::challenge(¶ms);
        let fake_response = BigUint::generate_random().unwrap();
        let verified = DiscreteLogChaumPedersen::verify(¶ms, &fake_response, &c, &cp);
        assert!(!verified);
    }
    #[test]
    fn test_fail_rfc_2048_256_bits_params() {
        let params = RFC5114_MODP_2048_256_BIT_PARAMS.to_owned();
        let mut rng = OsRng;
        let x = rng.gen_biguint_below(¶ms.p);
        let (cp, _) = DiscreteLogChaumPedersen::commitment(¶ms, &x);
        let c = DiscreteLogChaumPedersen::challenge(¶ms);
        let fake_response = BigUint::generate_random().unwrap();
        let verified = DiscreteLogChaumPedersen::verify(¶ms, &fake_response, &c, &cp);
        assert!(!verified);
    }
    #[test]
    fn test_verify() {
        let g = BigUint::from(4u32);
        let h = BigUint::from(9u32);
        let p = BigUint::from(23u32);
        let q = BigUint::from(11u32);
        let params = GroupParams::<BigUint> {
            g: g.clone(),
            h: h.clone(),
            p: p.clone(),
            q: q.clone(),
        };
        let cp = (
            BigUint::from(6u32),
            BigUint::from(18u32),
            BigUint::from(2u32),
            BigUint::from(3u32),
        );
        let x = BigUint::from(10u32);
        let k = BigUint::from(17u32);
        let c = BigUint::from(0u32);
        let s = DiscreteLogChaumPedersen::challenge_response(¶ms, &k, &c, &x);
        println!("Response: {:?}", s);
        assert!(DiscreteLogChaumPedersen::verify(¶ms, &s, &c, &cp));
    }
}