use algebra::{BitIterator, Field, FpParameters, PrimeField};

use crate::{prelude::*, Assignment};
use r1cs_core::{ConstraintSystem, LinearCombination, SynthesisError, Variable};
use std::borrow::Borrow;

/// Represents a variable in the constraint system which is guaranteed
/// to be either zero or one.
#[derive(Copy, Clone, Debug)]
pub struct AllocatedBit {
    variable: Variable,
    value:    Option<bool>,
}

impl AllocatedBit {
    pub fn get_value(&self) -> Option<bool> {
        self.value
    }

    pub fn get_variable(&self) -> Variable {
        self.variable
    }

    /// Performs an XOR operation over the two operands, returning
    /// an `AllocatedBit`.
    pub fn xor<ConstraintF, CS>(mut cs: CS, a: &Self, b: &Self) -> Result<Self, SynthesisError>
    where
        ConstraintF: Field,
        CS: ConstraintSystem<ConstraintF>,
    {
        let mut result_value = None;

        let result_var = cs.alloc(
            || "xor result",
            || {
                if a.value.get()? ^ b.value.get()? {
                    result_value = Some(true);

                    Ok(ConstraintF::one())
                } else {
                    result_value = Some(false);

                    Ok(ConstraintF::zero())
                }
            },
        )?;

        // Constrain (a + a) * (b) = (a + b - c)
        // Given that a and b are boolean constrained, if they
        // are equal, the only solution for c is 0, and if they
        // are different, the only solution for c is 1.
        //
        // ¬(a ∧ b) ∧ ¬(¬a ∧ ¬b) = c
        // (1 - (a * b)) * (1 - ((1 - a) * (1 - b))) = c
        // (1 - ab) * (1 - (1 - a - b + ab)) = c
        // (1 - ab) * (a + b - ab) = c
        // a + b - ab - (a^2)b - (b^2)a + (a^2)(b^2) = c
        // a + b - ab - ab - ab + ab = c
        // a + b - 2ab = c
        // -2a * b = c - a - b
        // 2a * b = a + b - c
        // (a + a) * b = a + b - c
        cs.enforce(
            || "xor constraint",
            |lc| lc + a.variable + a.variable,
            |lc| lc + b.variable,
            |lc| lc + a.variable + b.variable - result_var,
        );

        Ok(AllocatedBit {
            variable: result_var,
            value:    result_value,
        })
    }

    /// Performs an AND operation over the two operands, returning
    /// an `AllocatedBit`.
    pub fn and<ConstraintF, CS>(mut cs: CS, a: &Self, b: &Self) -> Result<Self, SynthesisError>
    where
        ConstraintF: Field,
        CS: ConstraintSystem<ConstraintF>,
    {
        let mut result_value = None;

        let result_var = cs.alloc(
            || "and result",
            || {
                if a.value.get()? & b.value.get()? {
                    result_value = Some(true);

                    Ok(ConstraintF::one())
                } else {
                    result_value = Some(false);

                    Ok(ConstraintF::zero())
                }
            },
        )?;

        // Constrain (a) * (b) = (c), ensuring c is 1 iff
        // a AND b are both 1.
        cs.enforce(
            || "and constraint",
            |lc| lc + a.variable,
            |lc| lc + b.variable,
            |lc| lc + result_var,
        );

        Ok(AllocatedBit {
            variable: result_var,
            value:    result_value,
        })
    }

    /// Performs an OR operation over the two operands, returning
    /// an `AllocatedBit`.
    pub fn or<ConstraintF, CS>(cs: CS, a: &Self, b: &Self) -> Result<Self, SynthesisError>
    where
        ConstraintF: Field,
        CS: ConstraintSystem<ConstraintF>,
    {
        Self::conditionally_select(cs, &Boolean::from(*a), a, b)
    }

    /// Calculates `a AND (NOT b)`.
    pub fn and_not<ConstraintF, CS>(mut cs: CS, a: &Self, b: &Self) -> Result<Self, SynthesisError>
    where
        ConstraintF: Field,
        CS: ConstraintSystem<ConstraintF>,
    {
        let mut result_value = None;

        let result_var = cs.alloc(
            || "and not result",
            || {
                if a.value.get()? & !b.value.get()? {
                    result_value = Some(true);

                    Ok(ConstraintF::one())
                } else {
                    result_value = Some(false);

                    Ok(ConstraintF::zero())
                }
            },
        )?;

        // Constrain (a) * (1 - b) = (c), ensuring c is 1 iff
        // a is true and b is false, and otherwise c is 0.
        cs.enforce(
            || "and not constraint",
            |lc| lc + a.variable,
            |lc| lc + CS::one() - b.variable,
            |lc| lc + result_var,
        );

        Ok(AllocatedBit {
            variable: result_var,
            value:    result_value,
        })
    }

    /// Calculates `(NOT a) AND (NOT b)`.
    pub fn nor<ConstraintF, CS>(mut cs: CS, a: &Self, b: &Self) -> Result<Self, SynthesisError>
    where
        ConstraintF: Field,
        CS: ConstraintSystem<ConstraintF>,
    {
        let mut result_value = None;

        let result_var = cs.alloc(
            || "nor result",
            || {
                if !a.value.get()? & !b.value.get()? {
                    result_value = Some(true);

                    Ok(ConstraintF::one())
                } else {
                    result_value = Some(false);

                    Ok(ConstraintF::zero())
                }
            },
        )?;

        // Constrain (1 - a) * (1 - b) = (c), ensuring c is 1 iff
        // a and b are both false, and otherwise c is 0.
        cs.enforce(
            || "nor constraint",
            |lc| lc + CS::one() - a.variable,
            |lc| lc + CS::one() - b.variable,
            |lc| lc + result_var,
        );

        Ok(AllocatedBit {
            variable: result_var,
            value:    result_value,
        })
    }
}

impl PartialEq for AllocatedBit {
    fn eq(&self, other: &Self) -> bool {
        self.value.is_some() && other.value.is_some() && self.value == other.value
    }
}

impl Eq for AllocatedBit {}

impl<ConstraintF: Field> AllocGadget<bool, ConstraintF> for AllocatedBit {
    fn alloc<F, T, CS: ConstraintSystem<ConstraintF>>(
        mut cs: CS,
        value_gen: F,
    ) -> Result<Self, SynthesisError>
    where
        F: FnOnce() -> Result<T, SynthesisError>,
        T: Borrow<bool>,
    {
        let mut value = None;
        let var = cs.alloc(
            || "boolean",
            || {
                value = Some(*value_gen()?.borrow());
                if value.get()? {
                    Ok(ConstraintF::one())
                } else {
                    Ok(ConstraintF::zero())
                }
            },
        )?;

        // Constrain: (1 - a) * a = 0
        // This constrains a to be either 0 or 1.
        cs.enforce(
            || "boolean constraint",
            |lc| lc + CS::one() - var,
            |lc| lc + var,
            |lc| lc,
        );

        Ok(AllocatedBit {
            variable: var,
            value,
        })
    }

    fn alloc_input<F, T, CS: ConstraintSystem<ConstraintF>>(
        mut cs: CS,
        value_gen: F,
    ) -> Result<Self, SynthesisError>
    where
        F: FnOnce() -> Result<T, SynthesisError>,
        T: Borrow<bool>,
    {
        let mut value = None;
        let var = cs.alloc_input(
            || "boolean",
            || {
                value = Some(*value_gen()?.borrow());
                if value.get()? {
                    Ok(ConstraintF::one())
                } else {
                    Ok(ConstraintF::zero())
                }
            },
        )?;

        // Constrain: (1 - a) * a = 0
        // This constrains a to be either 0 or 1.
        cs.enforce(
            || "boolean constraint",
            |lc| lc + CS::one() - var,
            |lc| lc + var,
            |lc| lc,
        );

        Ok(AllocatedBit {
            variable: var,
            value,
        })
    }
}

impl<ConstraintF: Field> CondSelectGadget<ConstraintF> for AllocatedBit {
    fn conditionally_select<CS: ConstraintSystem<ConstraintF>>(
        mut cs: CS,
        cond: &Boolean,
        first: &Self,
        second: &Self,
    ) -> Result<Self, SynthesisError> {
        let result = Self::alloc(cs.ns(|| ""), || {
            cond.get_value()
                .and_then(|cond| {
                    {
                        if cond {
                            first
                        } else {
                            second
                        }
                    }
                    .get_value()
                })
                .get()
        })?;

        // a = self; b = other; c = cond;
        //
        // r = c * a + (1  - c) * b
        // r = b + c * (a - b)
        // c * (a - b) = r - b
        let one = CS::one();
        cs.enforce(
            || "conditionally_select",
            |_| cond.lc(one, ConstraintF::one()),
            |lc| lc + first.variable - second.variable,
            |lc| lc + result.variable - second.variable,
        );

        Ok(result)
    }

    fn cost() -> usize {
        1
    }
}

/// This is a boolean value which may be either a constant or
/// an interpretation of an `AllocatedBit`.
#[derive(Copy, Clone, Debug)]
pub enum Boolean {
    /// Existential view of the boolean variable
    Is(AllocatedBit),
    /// Negated view of the boolean variable
    Not(AllocatedBit),
    /// Constant (not an allocated variable)
    Constant(bool),
}

impl Boolean {
    pub fn get_value(&self) -> Option<bool> {
        match *self {
            Boolean::Constant(c) => Some(c),
            Boolean::Is(ref v) => v.get_value(),
            Boolean::Not(ref v) => v.get_value().map(|b| !b),
        }
    }

    pub fn lc<ConstraintF: Field>(
        &self,
        one: Variable,
        coeff: ConstraintF,
    ) -> LinearCombination<ConstraintF> {
        match *self {
            Boolean::Constant(c) => {
                if c {
                    (coeff, one).into()
                } else {
                    LinearCombination::<ConstraintF>::zero()
                }
            },
            Boolean::Is(ref v) => (coeff, v.get_variable()).into(),
            Boolean::Not(ref v) => {
                LinearCombination::<ConstraintF>::zero() + (coeff, one) - (coeff, v.get_variable())
            },
        }
    }

    /// Construct a boolean vector from a vector of u8
    pub fn constant_u8_vec<ConstraintF: Field, CS: ConstraintSystem<ConstraintF>>(
        cs: &mut CS,
        values: &[u8],
    ) -> Vec<Self> {
        let mut input_bits = vec![];
        for (byte_i, input_byte) in values.iter().enumerate() {
            for bit_i in (0..8).rev() {
                let cs = cs.ns(|| format!("input_bit_gadget {} {}", byte_i, bit_i));
                input_bits.push(
                    AllocatedBit::alloc(cs, || Ok((input_byte >> bit_i) & 1u8 == 1u8))
                        .unwrap()
                        .into(),
                );
            }
        }
        input_bits
    }

    /// Construct a boolean from a known constant
    pub fn constant(b: bool) -> Self {
        Boolean::Constant(b)
    }

    /// Return a negated interpretation of this boolean.
    pub fn not(&self) -> Self {
        match *self {
            Boolean::Constant(c) => Boolean::Constant(!c),
            Boolean::Is(ref v) => Boolean::Not(*v),
            Boolean::Not(ref v) => Boolean::Is(*v),
        }
    }

    /// Perform XOR over two boolean operands
    pub fn xor<'a, ConstraintF, CS>(
        cs: CS,
        a: &'a Self,
        b: &'a Self,
    ) -> Result<Self, SynthesisError>
    where
        ConstraintF: Field,
        CS: ConstraintSystem<ConstraintF>,
    {
        match (a, b) {
            (&Boolean::Constant(false), x) | (x, &Boolean::Constant(false)) => Ok(*x),
            (&Boolean::Constant(true), x) | (x, &Boolean::Constant(true)) => Ok(x.not()),
            // a XOR (NOT b) = NOT(a XOR b)
            (is @ &Boolean::Is(_), not @ &Boolean::Not(_))
            | (not @ &Boolean::Not(_), is @ &Boolean::Is(_)) => {
                Ok(Boolean::xor(cs, is, &not.not())?.not())
            },
            // a XOR b = (NOT a) XOR (NOT b)
            (&Boolean::Is(ref a), &Boolean::Is(ref b))
            | (&Boolean::Not(ref a), &Boolean::Not(ref b)) => {
                Ok(Boolean::Is(AllocatedBit::xor(cs, a, b)?))
            },
        }
    }

    /// Perform OR over two boolean operands
    pub fn or<'a, ConstraintF, CS>(cs: CS, a: &'a Self, b: &'a Self) -> Result<Self, SynthesisError>
    where
        ConstraintF: Field,
        CS: ConstraintSystem<ConstraintF>,
    {
        match (a, b) {
            (&Boolean::Constant(false), x) | (x, &Boolean::Constant(false)) => Ok(*x),
            (&Boolean::Constant(true), _) | (_, &Boolean::Constant(true)) => {
                Ok(Boolean::Constant(true))
            },
            // a OR b = NOT ((NOT a) AND b)
            (a @ &Boolean::Is(_), b @ &Boolean::Not(_))
            | (b @ &Boolean::Not(_), a @ &Boolean::Is(_))
            | (b @ &Boolean::Not(_), a @ &Boolean::Not(_)) => {
                Ok(Boolean::and(cs, &a.not(), &b.not())?.not())
            },
            (&Boolean::Is(ref a), &Boolean::Is(ref b)) => {
                AllocatedBit::or(cs, a, b).map(Boolean::from)
            },
        }
    }

    /// Perform AND over two boolean operands
    pub fn and<'a, ConstraintF, CS>(
        cs: CS,
        a: &'a Self,
        b: &'a Self,
    ) -> Result<Self, SynthesisError>
    where
        ConstraintF: Field,
        CS: ConstraintSystem<ConstraintF>,
    {
        match (a, b) {
            // false AND x is always false
            (&Boolean::Constant(false), _) | (_, &Boolean::Constant(false)) => {
                Ok(Boolean::Constant(false))
            },
            // true AND x is always x
            (&Boolean::Constant(true), x) | (x, &Boolean::Constant(true)) => Ok(*x),
            // a AND (NOT b)
            (&Boolean::Is(ref is), &Boolean::Not(ref not))
            | (&Boolean::Not(ref not), &Boolean::Is(ref is)) => {
                Ok(Boolean::Is(AllocatedBit::and_not(cs, is, not)?))
            },
            // (NOT a) AND (NOT b) = a NOR b
            (&Boolean::Not(ref a), &Boolean::Not(ref b)) => {
                Ok(Boolean::Is(AllocatedBit::nor(cs, a, b)?))
            },
            // a AND b
            (&Boolean::Is(ref a), &Boolean::Is(ref b)) => {
                Ok(Boolean::Is(AllocatedBit::and(cs, a, b)?))
            },
        }
    }

    pub fn kary_and<ConstraintF, CS>(mut cs: CS, bits: &[Self]) -> Result<Self, SynthesisError>
    where
        ConstraintF: Field,
        CS: ConstraintSystem<ConstraintF>,
    {
        assert!(!bits.is_empty());
        let mut bits = bits.iter();

        let mut cur: Self = *bits.next().unwrap();
        for (i, next) in bits.enumerate() {
            cur = Boolean::and(cs.ns(|| format!("AND {}", i)), &cur, next)?;
        }

        Ok(cur)
    }

    /// Asserts that at least one operand is false.
    pub fn enforce_nand<ConstraintF, CS>(mut cs: CS, bits: &[Self]) -> Result<(), SynthesisError>
    where
        ConstraintF: Field,
        CS: ConstraintSystem<ConstraintF>,
    {
        let res = Self::kary_and(&mut cs, bits)?;

        match res {
            Boolean::Constant(false) => Ok(()),
            Boolean::Constant(true) => Err(SynthesisError::AssignmentMissing),
            Boolean::Is(ref res) => {
                cs.enforce(
                    || "enforce nand",
                    |lc| lc,
                    |lc| lc,
                    |lc| lc + res.get_variable(),
                );

                Ok(())
            },
            Boolean::Not(ref res) => {
                cs.enforce(
                    || "enforce nand",
                    |lc| lc,
                    |lc| lc,
                    |lc| lc + CS::one() - res.get_variable(),
                );

                Ok(())
            },
        }
    }

    /// Asserts that this bit_gadget representation is "in
    /// the field" when interpreted in big endian.
    pub fn enforce_in_field<ConstraintF, CS, F: PrimeField>(
        mut cs: CS,
        bits: &[Self],
    ) -> Result<(), SynthesisError>
    where
        ConstraintF: Field,
        CS: ConstraintSystem<ConstraintF>,
    {
        let mut bits_iter = bits.iter();

        // b = char() - 1
        let mut b = F::characteristic().to_vec();
        assert_eq!(b[0] % 2, 1);
        b[0] -= 1;

        // Runs of ones in r
        let mut last_run = Boolean::constant(true);
        let mut current_run = vec![];

        let mut found_one = false;
        let mut run_i = 0;
        let mut nand_i = 0;

        let char_num_bits = <F as PrimeField>::Params::MODULUS_BITS as usize;
        if bits.len() > char_num_bits {
            let num_extra_bits = bits.len() - char_num_bits;
            let mut or_result = Boolean::constant(false);
            for (i, should_be_zero) in bits[0..num_extra_bits].iter().enumerate() {
                or_result = Boolean::or(
                    &mut cs.ns(|| format!("Check {}-th or", i)),
                    &or_result,
                    should_be_zero,
                )?;
                let _ = bits_iter.next().unwrap();
            }
            or_result.enforce_equal(
                &mut cs.ns(|| "Check that or of extra bits is zero"),
                &Boolean::constant(false),
            )?;
        }

        for b in BitIterator::new(b) {
            // Skip over unset bits at the beginning
            found_one |= b;
            if !found_one {
                continue;
            }

            let a = bits_iter.next().unwrap();

            if b {
                // This is part of a run of ones.
                current_run.push(a.clone());
            } else {
                if !current_run.is_empty() {
                    // This is the start of a run of zeros, but we need
                    // to k-ary AND against `last_run` first.

                    current_run.push(last_run);
                    last_run = Self::kary_and(cs.ns(|| format!("run {}", run_i)), &current_run)?;
                    run_i += 1;
                    current_run.truncate(0);
                }

                // If `last_run` is true, `a` must be false, or it would
                // not be in the field.
                //
                // If `last_run` is false, `a` can be true or false.
                //
                // Ergo, at least one of `last_run` and `a` must be false.
                Self::enforce_nand(cs.ns(|| format!("nand {}", nand_i)), &[last_run, *a])?;
                nand_i += 1;
            }
        }
        assert!(bits_iter.next().is_none());

        // We should always end in a "run" of zeros, because
        // the characteristic is an odd prime. So, this should
        // be empty.
        assert!(current_run.is_empty());

        Ok(())
    }
}

impl PartialEq for Boolean {
    fn eq(&self, other: &Self) -> bool {
        use self::Boolean::*;

        match (*self, *other) {
            (Is(a), Is(b)) | (Not(a), Not(b)) => a == b,
            (Is(a), Not(b)) | (Not(a), Is(b)) => a != b,
            (Is(a), Constant(b)) | (Constant(b), Is(a)) => a.value.unwrap() == b,
            (Not(a), Constant(b)) | (Constant(b), Not(a)) => a.value.unwrap() != b,
            (Constant(a), Constant(b)) => a == b,
        }
    }
}

impl Eq for Boolean {}

impl From<AllocatedBit> for Boolean {
    fn from(b: AllocatedBit) -> Boolean {
        Boolean::Is(b)
    }
}

impl<ConstraintF: Field> AllocGadget<bool, ConstraintF> for Boolean {
    fn alloc<F, T, CS: ConstraintSystem<ConstraintF>>(
        cs: CS,
        value_gen: F,
    ) -> Result<Self, SynthesisError>
    where
        F: FnOnce() -> Result<T, SynthesisError>,
        T: Borrow<bool>,
    {
        AllocatedBit::alloc(cs, value_gen).map(Boolean::from)
    }

    fn alloc_input<F, T, CS: ConstraintSystem<ConstraintF>>(
        cs: CS,
        value_gen: F,
    ) -> Result<Self, SynthesisError>
    where
        F: FnOnce() -> Result<T, SynthesisError>,
        T: Borrow<bool>,
    {
        AllocatedBit::alloc_input(cs, value_gen).map(Boolean::from)
    }
}

impl<ConstraintF: Field> EqGadget<ConstraintF> for Boolean {}

impl<ConstraintF: Field> ConditionalEqGadget<ConstraintF> for Boolean {
    fn conditional_enforce_equal<CS>(
        &self,
        mut cs: CS,
        other: &Self,
        condition: &Boolean,
    ) -> Result<(), SynthesisError>
    where
        CS: ConstraintSystem<ConstraintF>,
    {
        use self::Boolean::*;
        let one = CS::one();
        let difference: LinearCombination<ConstraintF> = match (self, other) {
            // 1 - 1 = 0 - 0 = 0
            (Constant(true), Constant(true)) | (Constant(false), Constant(false)) => return Ok(()),
            // false != true
            (Constant(_), Constant(_)) => return Err(SynthesisError::AssignmentMissing),
            // 1 - a
            (Constant(true), Is(a)) | (Is(a), Constant(true)) => {
                LinearCombination::zero() + one - a.get_variable()
            },
            // a - 0 = a
            (Constant(false), Is(a)) | (Is(a), Constant(false)) => {
                LinearCombination::zero() + a.get_variable()
            },
            // 1 - !a = 1 - (1 - a) = a
            (Constant(true), Not(a)) | (Not(a), Constant(true)) => {
                LinearCombination::zero() + a.get_variable()
            },
            // !a - 0 = !a = 1 - a
            (Constant(false), Not(a)) | (Not(a), Constant(false)) => {
                LinearCombination::zero() + one - a.get_variable()
            },
            // b - a,
            (Is(a), Is(b)) => LinearCombination::zero() + b.get_variable() - a.get_variable(),
            // !b - a = (1 - b) - a
            (Is(a), Not(b)) | (Not(b), Is(a)) => {
                LinearCombination::zero() + one - b.get_variable() - a.get_variable()
            },
            // !b - !a = (1 - b) - (1 - a) = a - b,
            (Not(a), Not(b)) => LinearCombination::zero() + a.get_variable() - b.get_variable(),
        };

        if let Constant(false) = condition {
            Ok(())
        } else {
            cs.enforce(
                || "conditional_equals",
                |lc| difference + &lc,
                |lc| condition.lc(one, ConstraintF::one()) + &lc,
                |lc| lc,
            );
            Ok(())
        }
    }

    fn cost() -> usize {
        1
    }
}

impl<ConstraintF: Field> ToBytesGadget<ConstraintF> for Boolean {
    fn to_bytes<CS: ConstraintSystem<ConstraintF>>(
        &self,
        _cs: CS,
    ) -> Result<Vec<UInt8>, SynthesisError> {
        let mut bits = vec![Boolean::constant(false); 7];
        bits.push(*self);
        bits.reverse();
        let value = self.get_value().map(|val| val as u8);
        let byte = UInt8 { bits, value };
        Ok(vec![byte])
    }

    /// Additionally checks if the produced list of booleans is 'valid'.
    fn to_bytes_strict<CS: ConstraintSystem<ConstraintF>>(
        &self,
        cs: CS,
    ) -> Result<Vec<UInt8>, SynthesisError> {
        self.to_bytes(cs)
    }
}

#[cfg(test)]
mod test {
    use super::{AllocatedBit, Boolean};
    use crate::{prelude::*, test_constraint_system::TestConstraintSystem};
    use algebra::{fields::bls12_381::Fr, BitIterator, Field, PrimeField, UniformRand};
    use r1cs_core::ConstraintSystem;
    use rand::SeedableRng;
    use rand_xorshift::XorShiftRng;
    use std::str::FromStr;

    #[test]
    fn test_boolean_to_byte() {
        for val in [true, false].iter() {
            let mut cs = TestConstraintSystem::<Fr>::new();
            let a: Boolean = AllocatedBit::alloc(&mut cs, || Ok(*val)).unwrap().into();
            let bytes = a.to_bytes(&mut cs.ns(|| "ToBytes")).unwrap();
            assert_eq!(bytes.len(), 1);
            let byte = &bytes[0];
            assert_eq!(byte.value.unwrap(), *val as u8);

            for (i, bit_gadget) in byte.bits.iter().enumerate() {
                assert_eq!(
                    bit_gadget.get_value().unwrap(),
                    (byte.value.unwrap() >> i) & 1 == 1
                );
            }
        }
    }

    #[test]
    fn test_allocated_bit() {
        let mut cs = TestConstraintSystem::<Fr>::new();

        AllocatedBit::alloc(&mut cs, || Ok(true)).unwrap();
        assert!(cs.get("boolean") == Fr::one());
        assert!(cs.is_satisfied());
        cs.set("boolean", Fr::zero());
        assert!(cs.is_satisfied());
        cs.set("boolean", Fr::from_str("2").unwrap());
        assert!(!cs.is_satisfied());
        assert!(cs.which_is_unsatisfied() == Some("boolean constraint"));
    }

    #[test]
    fn test_xor() {
        for a_val in [false, true].iter() {
            for b_val in [false, true].iter() {
                let mut cs = TestConstraintSystem::<Fr>::new();
                let a = AllocatedBit::alloc(cs.ns(|| "a"), || Ok(*a_val)).unwrap();
                let b = AllocatedBit::alloc(cs.ns(|| "b"), || Ok(*b_val)).unwrap();
                let c = AllocatedBit::xor(&mut cs, &a, &b).unwrap();
                assert_eq!(c.value.unwrap(), *a_val ^ *b_val);

                assert!(cs.is_satisfied());
            }
        }
    }

    #[test]
    fn test_or() {
        for a_val in [false, true].iter() {
            for b_val in [false, true].iter() {
                let mut cs = TestConstraintSystem::<Fr>::new();
                let a = AllocatedBit::alloc(cs.ns(|| "a"), || Ok(*a_val)).unwrap();
                let b = AllocatedBit::alloc(cs.ns(|| "b"), || Ok(*b_val)).unwrap();
                let c = AllocatedBit::or(&mut cs, &a, &b).unwrap();
                assert_eq!(c.value.unwrap(), *a_val | *b_val);

                assert!(cs.is_satisfied());
                assert!(cs.get("a/boolean") == if *a_val { Field::one() } else { Field::zero() });
                assert!(cs.get("b/boolean") == if *b_val { Field::one() } else { Field::zero() });
            }
        }
    }

    #[test]
    fn test_and() {
        for a_val in [false, true].iter() {
            for b_val in [false, true].iter() {
                let mut cs = TestConstraintSystem::<Fr>::new();
                let a = AllocatedBit::alloc(cs.ns(|| "a"), || Ok(*a_val)).unwrap();
                let b = AllocatedBit::alloc(cs.ns(|| "b"), || Ok(*b_val)).unwrap();
                let c = AllocatedBit::and(&mut cs, &a, &b).unwrap();
                assert_eq!(c.value.unwrap(), *a_val & *b_val);

                assert!(cs.is_satisfied());
                assert!(cs.get("a/boolean") == if *a_val { Field::one() } else { Field::zero() });
                assert!(cs.get("b/boolean") == if *b_val { Field::one() } else { Field::zero() });
                assert!(
                    cs.get("and result")
                        == if *a_val & *b_val {
                            Field::one()
                        } else {
                            Field::zero()
                        }
                );

                // Invert the result and check if the constraint system is still satisfied
                cs.set(
                    "and result",
                    if *a_val & *b_val {
                        Field::zero()
                    } else {
                        Field::one()
                    },
                );
                assert!(!cs.is_satisfied());
            }
        }
    }

    #[test]
    fn test_and_not() {
        for a_val in [false, true].iter() {
            for b_val in [false, true].iter() {
                let mut cs = TestConstraintSystem::<Fr>::new();
                let a = AllocatedBit::alloc(cs.ns(|| "a"), || Ok(*a_val)).unwrap();
                let b = AllocatedBit::alloc(cs.ns(|| "b"), || Ok(*b_val)).unwrap();
                let c = AllocatedBit::and_not(&mut cs, &a, &b).unwrap();
                assert_eq!(c.value.unwrap(), *a_val & !*b_val);

                assert!(cs.is_satisfied());
                assert!(cs.get("a/boolean") == if *a_val { Field::one() } else { Field::zero() });
                assert!(cs.get("b/boolean") == if *b_val { Field::one() } else { Field::zero() });
                assert!(
                    cs.get("and not result")
                        == if *a_val & !*b_val {
                            Field::one()
                        } else {
                            Field::zero()
                        }
                );

                // Invert the result and check if the constraint system is still satisfied
                cs.set(
                    "and not result",
                    if *a_val & !*b_val {
                        Field::zero()
                    } else {
                        Field::one()
                    },
                );
                assert!(!cs.is_satisfied());
            }
        }
    }

    #[test]
    fn test_nor() {
        for a_val in [false, true].iter() {
            for b_val in [false, true].iter() {
                let mut cs = TestConstraintSystem::<Fr>::new();
                let a = AllocatedBit::alloc(cs.ns(|| "a"), || Ok(*a_val)).unwrap();
                let b = AllocatedBit::alloc(cs.ns(|| "b"), || Ok(*b_val)).unwrap();
                let c = AllocatedBit::nor(&mut cs, &a, &b).unwrap();
                assert_eq!(c.value.unwrap(), !*a_val & !*b_val);

                assert!(cs.is_satisfied());
                assert!(cs.get("a/boolean") == if *a_val { Field::one() } else { Field::zero() });
                assert!(cs.get("b/boolean") == if *b_val { Field::one() } else { Field::zero() });
                assert!(
                    cs.get("nor result")
                        == if !*a_val & !*b_val {
                            Field::one()
                        } else {
                            Field::zero()
                        }
                );

                // Invert the result and check if the constraint system is still satisfied
                cs.set(
                    "nor result",
                    if !*a_val & !*b_val {
                        Field::zero()
                    } else {
                        Field::one()
                    },
                );
                assert!(!cs.is_satisfied());
            }
        }
    }

    #[test]
    fn test_enforce_equal() {
        for a_bool in [false, true].iter().cloned() {
            for b_bool in [false, true].iter().cloned() {
                for a_neg in [false, true].iter().cloned() {
                    for b_neg in [false, true].iter().cloned() {
                        let mut cs = TestConstraintSystem::<Fr>::new();

                        let mut a: Boolean = AllocatedBit::alloc(cs.ns(|| "a"), || Ok(a_bool))
                            .unwrap()
                            .into();
                        let mut b: Boolean = AllocatedBit::alloc(cs.ns(|| "b"), || Ok(b_bool))
                            .unwrap()
                            .into();

                        if a_neg {
                            a = a.not();
                        }
                        if b_neg {
                            b = b.not();
                        }

                        a.enforce_equal(&mut cs, &b).unwrap();

                        assert_eq!(cs.is_satisfied(), (a_bool ^ a_neg) == (b_bool ^ b_neg));
                    }
                }
            }
        }
    }

    #[test]
    fn test_conditional_enforce_equal() {
        for a_bool in [false, true].iter().cloned() {
            for b_bool in [false, true].iter().cloned() {
                for a_neg in [false, true].iter().cloned() {
                    for b_neg in [false, true].iter().cloned() {
                        let mut cs = TestConstraintSystem::<Fr>::new();

                        // First test if constraint system is satisfied
                        // when we do want to enforce the condition.
                        let mut a: Boolean = AllocatedBit::alloc(cs.ns(|| "a"), || Ok(a_bool))
                            .unwrap()
                            .into();
                        let mut b: Boolean = AllocatedBit::alloc(cs.ns(|| "b"), || Ok(b_bool))
                            .unwrap()
                            .into();

                        if a_neg {
                            a = a.not();
                        }
                        if b_neg {
                            b = b.not();
                        }

                        a.conditional_enforce_equal(&mut cs, &b, &Boolean::constant(true))
                            .unwrap();

                        assert_eq!(cs.is_satisfied(), (a_bool ^ a_neg) == (b_bool ^ b_neg));

                        // Now test if constraint system is satisfied even
                        // when we don't want to enforce the condition.
                        let mut cs = TestConstraintSystem::<Fr>::new();

                        let mut a: Boolean = AllocatedBit::alloc(cs.ns(|| "a"), || Ok(a_bool))
                            .unwrap()
                            .into();
                        let mut b: Boolean = AllocatedBit::alloc(cs.ns(|| "b"), || Ok(b_bool))
                            .unwrap()
                            .into();

                        if a_neg {
                            a = a.not();
                        }
                        if b_neg {
                            b = b.not();
                        }

                        let false_cond = AllocatedBit::alloc(cs.ns(|| "cond"), || Ok(false))
                            .unwrap()
                            .into();
                        a.conditional_enforce_equal(&mut cs, &b, &false_cond)
                            .unwrap();

                        assert!(cs.is_satisfied());
                    }
                }
            }
        }
    }

    #[test]
    fn test_boolean_negation() {
        let mut cs = TestConstraintSystem::<Fr>::new();

        let mut b = Boolean::from(AllocatedBit::alloc(&mut cs, || Ok(true)).unwrap());

        match b {
            Boolean::Is(_) => {},
            _ => panic!("unexpected value"),
        }

        b = b.not();

        match b {
            Boolean::Not(_) => {},
            _ => panic!("unexpected value"),
        }

        b = b.not();

        match b {
            Boolean::Is(_) => {},
            _ => panic!("unexpected value"),
        }

        b = Boolean::constant(true);

        match b {
            Boolean::Constant(true) => {},
            _ => panic!("unexpected value"),
        }

        b = b.not();

        match b {
            Boolean::Constant(false) => {},
            _ => panic!("unexpected value"),
        }

        b = b.not();

        match b {
            Boolean::Constant(true) => {},
            _ => panic!("unexpected value"),
        }
    }

    #[derive(Copy, Clone, Debug)]
    enum OperandType {
        True,
        False,
        AllocatedTrue,
        AllocatedFalse,
        NegatedAllocatedTrue,
        NegatedAllocatedFalse,
    }

    #[test]
    fn test_boolean_xor() {
        let variants = [
            OperandType::True,
            OperandType::False,
            OperandType::AllocatedTrue,
            OperandType::AllocatedFalse,
            OperandType::NegatedAllocatedTrue,
            OperandType::NegatedAllocatedFalse,
        ];

        for first_operand in variants.iter().cloned() {
            for second_operand in variants.iter().cloned() {
                let mut cs = TestConstraintSystem::<Fr>::new();

                let a;
                let b;

                {
                    let mut dyn_construct = |operand, name| {
                        let cs = cs.ns(|| name);

                        match operand {
                            OperandType::True => Boolean::constant(true),
                            OperandType::False => Boolean::constant(false),
                            OperandType::AllocatedTrue => {
                                Boolean::from(AllocatedBit::alloc(cs, || Ok(true)).unwrap())
                            },
                            OperandType::AllocatedFalse => {
                                Boolean::from(AllocatedBit::alloc(cs, || Ok(false)).unwrap())
                            },
                            OperandType::NegatedAllocatedTrue => {
                                Boolean::from(AllocatedBit::alloc(cs, || Ok(true)).unwrap()).not()
                            },
                            OperandType::NegatedAllocatedFalse => {
                                Boolean::from(AllocatedBit::alloc(cs, || Ok(false)).unwrap()).not()
                            },
                        }
                    };

                    a = dyn_construct(first_operand, "a");
                    b = dyn_construct(second_operand, "b");
                }

                let c = Boolean::xor(&mut cs, &a, &b).unwrap();

                assert!(cs.is_satisfied());

                match (first_operand, second_operand, c) {
                    (OperandType::True, OperandType::True, Boolean::Constant(false)) => {},
                    (OperandType::True, OperandType::False, Boolean::Constant(true)) => {},
                    (OperandType::True, OperandType::AllocatedTrue, Boolean::Not(_)) => {},
                    (OperandType::True, OperandType::AllocatedFalse, Boolean::Not(_)) => {},
                    (OperandType::True, OperandType::NegatedAllocatedTrue, Boolean::Is(_)) => {},
                    (OperandType::True, OperandType::NegatedAllocatedFalse, Boolean::Is(_)) => {},

                    (OperandType::False, OperandType::True, Boolean::Constant(true)) => {},
                    (OperandType::False, OperandType::False, Boolean::Constant(false)) => {},
                    (OperandType::False, OperandType::AllocatedTrue, Boolean::Is(_)) => {},
                    (OperandType::False, OperandType::AllocatedFalse, Boolean::Is(_)) => {},
                    (OperandType::False, OperandType::NegatedAllocatedTrue, Boolean::Not(_)) => {},
                    (OperandType::False, OperandType::NegatedAllocatedFalse, Boolean::Not(_)) => {},

                    (OperandType::AllocatedTrue, OperandType::True, Boolean::Not(_)) => {},
                    (OperandType::AllocatedTrue, OperandType::False, Boolean::Is(_)) => {},
                    (
                        OperandType::AllocatedTrue,
                        OperandType::AllocatedTrue,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::AllocatedTrue,
                        OperandType::AllocatedFalse,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::one());
                        assert_eq!(v.value, Some(true));
                    },
                    (
                        OperandType::AllocatedTrue,
                        OperandType::NegatedAllocatedTrue,
                        Boolean::Not(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::AllocatedTrue,
                        OperandType::NegatedAllocatedFalse,
                        Boolean::Not(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::one());
                        assert_eq!(v.value, Some(true));
                    },

                    (OperandType::AllocatedFalse, OperandType::True, Boolean::Not(_)) => {},
                    (OperandType::AllocatedFalse, OperandType::False, Boolean::Is(_)) => {},
                    (
                        OperandType::AllocatedFalse,
                        OperandType::AllocatedTrue,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::one());
                        assert_eq!(v.value, Some(true));
                    },
                    (
                        OperandType::AllocatedFalse,
                        OperandType::AllocatedFalse,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::AllocatedFalse,
                        OperandType::NegatedAllocatedTrue,
                        Boolean::Not(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::one());
                        assert_eq!(v.value, Some(true));
                    },
                    (
                        OperandType::AllocatedFalse,
                        OperandType::NegatedAllocatedFalse,
                        Boolean::Not(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },

                    (OperandType::NegatedAllocatedTrue, OperandType::True, Boolean::Is(_)) => {},
                    (OperandType::NegatedAllocatedTrue, OperandType::False, Boolean::Not(_)) => {},
                    (
                        OperandType::NegatedAllocatedTrue,
                        OperandType::AllocatedTrue,
                        Boolean::Not(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::NegatedAllocatedTrue,
                        OperandType::AllocatedFalse,
                        Boolean::Not(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::one());
                        assert_eq!(v.value, Some(true));
                    },
                    (
                        OperandType::NegatedAllocatedTrue,
                        OperandType::NegatedAllocatedTrue,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::NegatedAllocatedTrue,
                        OperandType::NegatedAllocatedFalse,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::one());
                        assert_eq!(v.value, Some(true));
                    },

                    (OperandType::NegatedAllocatedFalse, OperandType::True, Boolean::Is(_)) => {},
                    (OperandType::NegatedAllocatedFalse, OperandType::False, Boolean::Not(_)) => {},
                    (
                        OperandType::NegatedAllocatedFalse,
                        OperandType::AllocatedTrue,
                        Boolean::Not(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::one());
                        assert_eq!(v.value, Some(true));
                    },
                    (
                        OperandType::NegatedAllocatedFalse,
                        OperandType::AllocatedFalse,
                        Boolean::Not(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::NegatedAllocatedFalse,
                        OperandType::NegatedAllocatedTrue,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::one());
                        assert_eq!(v.value, Some(true));
                    },
                    (
                        OperandType::NegatedAllocatedFalse,
                        OperandType::NegatedAllocatedFalse,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("xor result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },

                    _ => panic!("this should never be encountered"),
                }
            }
        }
    }

    #[test]
    fn test_boolean_or() {
        let variants = [
            OperandType::True,
            OperandType::False,
            OperandType::AllocatedTrue,
            OperandType::AllocatedFalse,
            OperandType::NegatedAllocatedTrue,
            OperandType::NegatedAllocatedFalse,
        ];

        for first_operand in variants.iter().cloned() {
            for second_operand in variants.iter().cloned() {
                let mut cs = TestConstraintSystem::<Fr>::new();

                let a;
                let b;

                {
                    let mut dyn_construct = |operand, name| {
                        let cs = cs.ns(|| name);

                        match operand {
                            OperandType::True => Boolean::constant(true),
                            OperandType::False => Boolean::constant(false),
                            OperandType::AllocatedTrue => {
                                Boolean::from(AllocatedBit::alloc(cs, || Ok(true)).unwrap())
                            },
                            OperandType::AllocatedFalse => {
                                Boolean::from(AllocatedBit::alloc(cs, || Ok(false)).unwrap())
                            },
                            OperandType::NegatedAllocatedTrue => {
                                Boolean::from(AllocatedBit::alloc(cs, || Ok(true)).unwrap()).not()
                            },
                            OperandType::NegatedAllocatedFalse => {
                                Boolean::from(AllocatedBit::alloc(cs, || Ok(false)).unwrap()).not()
                            },
                        }
                    };

                    a = dyn_construct(first_operand, "a");
                    b = dyn_construct(second_operand, "b");
                }

                let c = Boolean::or(&mut cs, &a, &b).unwrap();

                assert!(cs.is_satisfied());

                match (first_operand, second_operand, c) {
                    (OperandType::True, OperandType::True, Boolean::Constant(true)) => {},
                    (OperandType::True, OperandType::False, Boolean::Constant(true)) => {},
                    (OperandType::True, OperandType::AllocatedTrue, Boolean::Constant(true)) => {},
                    (OperandType::True, OperandType::AllocatedFalse, Boolean::Constant(true)) => {},
                    (
                        OperandType::True,
                        OperandType::NegatedAllocatedTrue,
                        Boolean::Constant(true),
                    ) => {},
                    (
                        OperandType::True,
                        OperandType::NegatedAllocatedFalse,
                        Boolean::Constant(true),
                    ) => {},

                    (OperandType::False, OperandType::True, Boolean::Constant(true)) => {},
                    (OperandType::False, OperandType::False, Boolean::Constant(false)) => {},
                    (OperandType::False, OperandType::AllocatedTrue, Boolean::Is(_)) => {},
                    (OperandType::False, OperandType::AllocatedFalse, Boolean::Is(_)) => {},
                    (OperandType::False, OperandType::NegatedAllocatedTrue, Boolean::Not(_)) => {},
                    (OperandType::False, OperandType::NegatedAllocatedFalse, Boolean::Not(_)) => {},

                    (OperandType::AllocatedTrue, OperandType::True, Boolean::Constant(true)) => {},
                    (OperandType::AllocatedTrue, OperandType::False, Boolean::Is(_)) => {},
                    (
                        OperandType::AllocatedTrue,
                        OperandType::AllocatedTrue,
                        Boolean::Is(ref v),
                    ) => {
                        assert_eq!(v.value, Some(true));
                    },
                    (
                        OperandType::AllocatedTrue,
                        OperandType::AllocatedFalse,
                        Boolean::Is(ref v),
                    ) => {
                        assert_eq!(v.value, Some(true));
                    },
                    (
                        OperandType::AllocatedTrue,
                        OperandType::NegatedAllocatedTrue,
                        Boolean::Not(ref v),
                    ) => {
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::AllocatedTrue,
                        OperandType::NegatedAllocatedFalse,
                        Boolean::Not(ref v),
                    ) => {
                        assert_eq!(v.value, Some(false));
                    },

                    (OperandType::AllocatedFalse, OperandType::True, Boolean::Constant(true)) => {},
                    (OperandType::AllocatedFalse, OperandType::False, Boolean::Is(_)) => {},
                    (
                        OperandType::AllocatedFalse,
                        OperandType::AllocatedTrue,
                        Boolean::Is(ref v),
                    ) => {
                        assert_eq!(v.value, Some(true));
                    },
                    (
                        OperandType::AllocatedFalse,
                        OperandType::AllocatedFalse,
                        Boolean::Is(ref v),
                    ) => {
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::AllocatedFalse,
                        OperandType::NegatedAllocatedTrue,
                        Boolean::Not(ref v),
                    ) => {
                        assert_eq!(v.value, Some(true));
                    },
                    (
                        OperandType::AllocatedFalse,
                        OperandType::NegatedAllocatedFalse,
                        Boolean::Not(ref v),
                    ) => {
                        assert_eq!(v.value, Some(false));
                    },

                    (
                        OperandType::NegatedAllocatedTrue,
                        OperandType::True,
                        Boolean::Constant(true),
                    ) => {},
                    (OperandType::NegatedAllocatedTrue, OperandType::False, Boolean::Not(_)) => {},
                    (
                        OperandType::NegatedAllocatedTrue,
                        OperandType::AllocatedTrue,
                        Boolean::Not(ref v),
                    ) => {
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::NegatedAllocatedTrue,
                        OperandType::AllocatedFalse,
                        Boolean::Not(ref v),
                    ) => {
                        assert_eq!(v.value, Some(true));
                    },
                    (
                        OperandType::NegatedAllocatedTrue,
                        OperandType::NegatedAllocatedTrue,
                        Boolean::Not(ref v),
                    ) => {
                        assert_eq!(v.value, Some(true));
                    },
                    (
                        OperandType::NegatedAllocatedTrue,
                        OperandType::NegatedAllocatedFalse,
                        Boolean::Not(ref v),
                    ) => {
                        assert_eq!(v.value, Some(false));
                    },

                    (
                        OperandType::NegatedAllocatedFalse,
                        OperandType::True,
                        Boolean::Constant(true),
                    ) => {},
                    (OperandType::NegatedAllocatedFalse, OperandType::False, Boolean::Not(_)) => {},
                    (
                        OperandType::NegatedAllocatedFalse,
                        OperandType::AllocatedTrue,
                        Boolean::Not(ref v),
                    ) => {
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::NegatedAllocatedFalse,
                        OperandType::AllocatedFalse,
                        Boolean::Not(ref v),
                    ) => {
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::NegatedAllocatedFalse,
                        OperandType::NegatedAllocatedTrue,
                        Boolean::Not(ref v),
                    ) => {
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::NegatedAllocatedFalse,
                        OperandType::NegatedAllocatedFalse,
                        Boolean::Not(ref v),
                    ) => {
                        assert_eq!(v.value, Some(false));
                    },

                    _ => panic!(
                        "this should never be encountered, in case: (a = {:?}, b = {:?}, c = {:?})",
                        a, b, c
                    ),
                }
            }
        }
    }

    #[test]
    fn test_boolean_and() {
        let variants = [
            OperandType::True,
            OperandType::False,
            OperandType::AllocatedTrue,
            OperandType::AllocatedFalse,
            OperandType::NegatedAllocatedTrue,
            OperandType::NegatedAllocatedFalse,
        ];

        for first_operand in variants.iter().cloned() {
            for second_operand in variants.iter().cloned() {
                let mut cs = TestConstraintSystem::<Fr>::new();

                let a;
                let b;

                {
                    let mut dyn_construct = |operand, name| {
                        let cs = cs.ns(|| name);

                        match operand {
                            OperandType::True => Boolean::constant(true),
                            OperandType::False => Boolean::constant(false),
                            OperandType::AllocatedTrue => {
                                Boolean::from(AllocatedBit::alloc(cs, || Ok(true)).unwrap())
                            },
                            OperandType::AllocatedFalse => {
                                Boolean::from(AllocatedBit::alloc(cs, || Ok(false)).unwrap())
                            },
                            OperandType::NegatedAllocatedTrue => {
                                Boolean::from(AllocatedBit::alloc(cs, || Ok(true)).unwrap()).not()
                            },
                            OperandType::NegatedAllocatedFalse => {
                                Boolean::from(AllocatedBit::alloc(cs, || Ok(false)).unwrap()).not()
                            },
                        }
                    };

                    a = dyn_construct(first_operand, "a");
                    b = dyn_construct(second_operand, "b");
                }

                let c = Boolean::and(&mut cs, &a, &b).unwrap();

                assert!(cs.is_satisfied());

                match (first_operand, second_operand, c) {
                    (OperandType::True, OperandType::True, Boolean::Constant(true)) => {},
                    (OperandType::True, OperandType::False, Boolean::Constant(false)) => {},
                    (OperandType::True, OperandType::AllocatedTrue, Boolean::Is(_)) => {},
                    (OperandType::True, OperandType::AllocatedFalse, Boolean::Is(_)) => {},
                    (OperandType::True, OperandType::NegatedAllocatedTrue, Boolean::Not(_)) => {},
                    (OperandType::True, OperandType::NegatedAllocatedFalse, Boolean::Not(_)) => {},

                    (OperandType::False, OperandType::True, Boolean::Constant(false)) => {},
                    (OperandType::False, OperandType::False, Boolean::Constant(false)) => {},
                    (OperandType::False, OperandType::AllocatedTrue, Boolean::Constant(false)) => {
                    },
                    (OperandType::False, OperandType::AllocatedFalse, Boolean::Constant(false)) => {
                    },
                    (
                        OperandType::False,
                        OperandType::NegatedAllocatedTrue,
                        Boolean::Constant(false),
                    ) => {},
                    (
                        OperandType::False,
                        OperandType::NegatedAllocatedFalse,
                        Boolean::Constant(false),
                    ) => {},

                    (OperandType::AllocatedTrue, OperandType::True, Boolean::Is(_)) => {},
                    (OperandType::AllocatedTrue, OperandType::False, Boolean::Constant(false)) => {
                    },
                    (
                        OperandType::AllocatedTrue,
                        OperandType::AllocatedTrue,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("and result") == Field::one());
                        assert_eq!(v.value, Some(true));
                    },
                    (
                        OperandType::AllocatedTrue,
                        OperandType::AllocatedFalse,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("and result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::AllocatedTrue,
                        OperandType::NegatedAllocatedTrue,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("and not result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::AllocatedTrue,
                        OperandType::NegatedAllocatedFalse,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("and not result") == Field::one());
                        assert_eq!(v.value, Some(true));
                    },

                    (OperandType::AllocatedFalse, OperandType::True, Boolean::Is(_)) => {},
                    (OperandType::AllocatedFalse, OperandType::False, Boolean::Constant(false)) => {
                    },
                    (
                        OperandType::AllocatedFalse,
                        OperandType::AllocatedTrue,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("and result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::AllocatedFalse,
                        OperandType::AllocatedFalse,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("and result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::AllocatedFalse,
                        OperandType::NegatedAllocatedTrue,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("and not result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::AllocatedFalse,
                        OperandType::NegatedAllocatedFalse,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("and not result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },

                    (OperandType::NegatedAllocatedTrue, OperandType::True, Boolean::Not(_)) => {},
                    (
                        OperandType::NegatedAllocatedTrue,
                        OperandType::False,
                        Boolean::Constant(false),
                    ) => {},
                    (
                        OperandType::NegatedAllocatedTrue,
                        OperandType::AllocatedTrue,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("and not result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::NegatedAllocatedTrue,
                        OperandType::AllocatedFalse,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("and not result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::NegatedAllocatedTrue,
                        OperandType::NegatedAllocatedTrue,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("nor result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::NegatedAllocatedTrue,
                        OperandType::NegatedAllocatedFalse,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("nor result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },

                    (OperandType::NegatedAllocatedFalse, OperandType::True, Boolean::Not(_)) => {},
                    (
                        OperandType::NegatedAllocatedFalse,
                        OperandType::False,
                        Boolean::Constant(false),
                    ) => {},
                    (
                        OperandType::NegatedAllocatedFalse,
                        OperandType::AllocatedTrue,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("and not result") == Field::one());
                        assert_eq!(v.value, Some(true));
                    },
                    (
                        OperandType::NegatedAllocatedFalse,
                        OperandType::AllocatedFalse,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("and not result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::NegatedAllocatedFalse,
                        OperandType::NegatedAllocatedTrue,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("nor result") == Field::zero());
                        assert_eq!(v.value, Some(false));
                    },
                    (
                        OperandType::NegatedAllocatedFalse,
                        OperandType::NegatedAllocatedFalse,
                        Boolean::Is(ref v),
                    ) => {
                        assert!(cs.get("nor result") == Field::one());
                        assert_eq!(v.value, Some(true));
                    },

                    _ => {
                        panic!(
                            "unexpected behavior at {:?} AND {:?}",
                            first_operand, second_operand
                        );
                    },
                }
            }
        }
    }

    #[test]
    fn test_enforce_in_field() {
        {
            let mut cs = TestConstraintSystem::<Fr>::new();

            let mut bits = vec![];
            for (i, b) in BitIterator::new(Fr::characteristic()).skip(1).enumerate() {
                bits.push(Boolean::from(
                    AllocatedBit::alloc(cs.ns(|| format!("bit_gadget {}", i)), || Ok(b)).unwrap(),
                ));
            }

            Boolean::enforce_in_field::<_, _, Fr>(&mut cs, &bits).unwrap();

            assert!(!cs.is_satisfied());
        }

        let mut rng = XorShiftRng::seed_from_u64(1231275789u64);

        for _ in 0..1000 {
            let r = Fr::rand(&mut rng);
            let mut cs = TestConstraintSystem::<Fr>::new();

            let mut bits = vec![];
            for (i, b) in BitIterator::new(r.into_repr()).skip(1).enumerate() {
                bits.push(Boolean::from(
                    AllocatedBit::alloc(cs.ns(|| format!("bit_gadget {}", i)), || Ok(b)).unwrap(),
                ));
            }

            Boolean::enforce_in_field::<_, _, Fr>(&mut cs, &bits).unwrap();

            assert!(cs.is_satisfied());
        }

        // for _ in 0..1000 {
        //     // Sample a random element not in the field
        //     let r = loop {
        //         let mut a = Fr::rand(&mut rng).into_repr();
        //         let b = Fr::rand(&mut rng).into_repr();

        //         a.add_nocarry(&b);
        //         // we're shaving off the high bit_gadget later
        //         a.as_mut()[3] &= 0x7fffffffffffffff;
        //         if Fr::from_repr(a).is_err() {
        //             break a;
        //         }
        //     };

        //     let mut cs = TestConstraintSystem::<Fr>::new();

        //     let mut bits = vec![];
        //     for (i, b) in BitIterator::new(r).skip(1).enumerate() {
        //         bits.push(Boolean::from(
        //             AllocatedBit::alloc(cs.ns(|| format!("bit_gadget {}",
        // i)), Some(b))                 .unwrap(),
        //         ));
        //     }

        //     Boolean::enforce_in_field::<_, _, Fr>(&mut cs, &bits).unwrap();

        //     assert!(!cs.is_satisfied());
        // }
    }

    #[test]
    fn test_enforce_nand() {
        {
            let mut cs = TestConstraintSystem::<Fr>::new();

            assert!(Boolean::enforce_nand(&mut cs, &[Boolean::constant(false)]).is_ok());
            assert!(Boolean::enforce_nand(&mut cs, &[Boolean::constant(true)]).is_err());
        }

        for i in 1..5 {
            // with every possible assignment for them
            for mut b in 0..(1 << i) {
                // with every possible negation
                for mut n in 0..(1 << i) {
                    let mut cs = TestConstraintSystem::<Fr>::new();

                    let mut expected = true;

                    let mut bits = vec![];
                    for j in 0..i {
                        expected &= b & 1 == 1;

                        if n & 1 == 1 {
                            bits.push(Boolean::from(
                                AllocatedBit::alloc(cs.ns(|| format!("bit_gadget {}", j)), || {
                                    Ok(b & 1 == 1)
                                })
                                .unwrap(),
                            ));
                        } else {
                            bits.push(
                                Boolean::from(
                                    AllocatedBit::alloc(
                                        cs.ns(|| format!("bit_gadget {}", j)),
                                        || Ok(b & 1 == 0),
                                    )
                                    .unwrap(),
                                )
                                .not(),
                            );
                        }

                        b >>= 1;
                        n >>= 1;
                    }

                    let expected = !expected;

                    Boolean::enforce_nand(&mut cs, &bits).unwrap();

                    if expected {
                        assert!(cs.is_satisfied());
                    } else {
                        assert!(!cs.is_satisfied());
                    }
                }
            }
        }
    }

    #[test]
    fn test_kary_and() {
        // test different numbers of operands
        for i in 1..15 {
            // with every possible assignment for them
            for mut b in 0..(1 << i) {
                let mut cs = TestConstraintSystem::<Fr>::new();

                let mut expected = true;

                let mut bits = vec![];
                for j in 0..i {
                    expected &= b & 1 == 1;

                    bits.push(Boolean::from(
                        AllocatedBit::alloc(cs.ns(|| format!("bit_gadget {}", j)), || {
                            Ok(b & 1 == 1)
                        })
                        .unwrap(),
                    ));
                    b >>= 1;
                }

                let r = Boolean::kary_and(&mut cs, &bits).unwrap();

                assert!(cs.is_satisfied());

                match r {
                    Boolean::Is(ref r) => {
                        assert_eq!(r.value.unwrap(), expected);
                    },
                    _ => unreachable!(),
                }
            }
        }
    }
}
