/// Modular exponentiation for a value.
pub trait ModExp<T> {
    /// Modular exponentiation using the given modulus type. If it's possible,
    /// we suggest using Barrett values, which are much faster than doing
    /// modulo with the number types.
    fn modexp(&self, e: &Self, m: &T) -> Self;
}

macro_rules! modexp_impls {
    ($name: ident, $other: ident) => {
        impl ModExp<$other> for $name {
            fn modexp(&self, ine: &$name, m: &$other) -> $name {
                // S <- g
                let mut s = self.clone();
                // A <- 1
                let mut a = $name::from(1u64);
                // We do a quick skim through and find the highest index that
                // actually has a value in it.
                let mut e = ine.clone();
                // While e != 0 do the following:
                while e.value.iter().any(|x| *x != 0) {
                    // If e is odd then A <- A * S
                    if e.value[0] & 1 != 0 {
                        a = a.modmul(&s, m);
                    }
                    // e <- floor(e / 2)
                    let mut carry = 0;
                    e.value.iter_mut().rev().for_each(|x| {
                        let new_carry = *x & 1;
                        *x = (*x >> 1) | (carry << 63);
                        carry = new_carry;
                    });
                    // If e != 0 then S <- S * S
                    s = s.modsq(m);
                }
                // Return A
                a
            }
        }
    };
}

#[cfg(test)]
macro_rules! generate_modexp_tests {
    ($name: ident, $lname: ident) => {
        #[test]
        fn $lname() {
            generate_modexp_tests!(body $name, $lname);
        }
    };
    (ignore $name: ident, $lname: ident) => {
        #[test]
        #[ignore]
        fn $lname() {
            generate_modexp_tests!(body $name, $lname);
        }
    };
    (body $name: ident, $lname: ident) => {
        let fname = build_test_path("modexp", stringify!($name));
        run_test(fname.to_string(), 6, |case| {
            let (neg0, bbytes) = case.get("b").unwrap();
            let (neg1, ebytes) = case.get("e").unwrap();
            let (neg2, mbytes) = case.get("m").unwrap();
            let (neg3, rbytes) = case.get("r").unwrap();
            assert!(!neg0 && !neg1 && !neg2 && !neg3);

            let b = $name::from_bytes(bbytes);
            let e = $name::from_bytes(ebytes);
            let m = $name::from_bytes(mbytes);
            let r = $name::from_bytes(rbytes);
            assert_eq!(r, b.modexp(&e, &m));
        });
    };
}

#[cfg(test)]
macro_rules! generate_barrett_modexp_tests {
    (ignore $name: ident, $lname: ident, $bname: ident) => {
        #[test]
        #[ignore]
        fn $lname() {
            generate_barrett_modexp_tests!(body $name, $lname, $bname);
        }
    };
    ($name: ident, $lname: ident, $bname: ident) => {
        #[test]
        fn $lname() {
            generate_barrett_modexp_tests!(body $name, $lname, $bname);
        }
    };
    (body $name: ident, $lname: ident, $bname: ident) => {
        let fname = build_test_path("modexp", stringify!($name));
        run_test(fname.to_string(), 6, |case| {
            let (neg0, bbytes) = case.get("b").unwrap();
            let (neg1, ebytes) = case.get("e").unwrap();
            let (neg2, mbytes) = case.get("m").unwrap();
            let (neg3, rbytes) = case.get("r").unwrap();
            let (neg4, kbytes) = case.get("k").unwrap();
            let (neg5, ubytes) = case.get("u").unwrap();
            assert!(!neg0 && !neg1 && !neg2 && !neg3 && !neg4 && !neg5);

            let b    = $name::from_bytes(bbytes);
            let e    = $name::from_bytes(ebytes);
            let m    = $name::from_bytes(mbytes);
            let r    = $name::from_bytes(rbytes);
            let kbig = $name::from_bytes(kbytes);
            let k    = usize::from(kbig);
            let mu   = $bname::from_bytes(ubytes);
            let bar  = $name::new_barrett(k, $bname::from(m), mu);

            if k == b.value.len() {
                assert_eq!(r, b.modexp(&e, &bar));
            }
        });
    };
}