use bellman::{
    gadgets::multipack,
    groth16::{create_random_proof, verify_proof, Parameters, PreparedVerifyingKey, Proof},
};
use ff::Field;
use pairing::bls12_381::{Bls12, Fr};
use rand_core::OsRng;
use std::ops::{AddAssign, Neg};
use zcash_primitives::{
    jubjub::{edwards, fs::Fs, FixedGenerators, JubjubBls12, Unknown},
    primitives::{Diversifier, Note, PaymentAddress, ProofGenerationKey, ValueCommitment},
};
use zcash_primitives::{
    merkle_tree::MerklePath,
    redjubjub::{PrivateKey, PublicKey, Signature},
    sapling::Node,
    transaction::components::Amount,
};

use super::compute_value_balance;
use crate::circuit::sapling::{Output, Spend};

/// A context object for creating the Sapling components of a Zcash transaction.
pub struct SaplingProvingContext {
    bsk: Fs,
    // (sum of the Spend value commitments) - (sum of the Output value commitments)
    cv_sum: edwards::Point<Bls12, Unknown>,
}

impl SaplingProvingContext {
    /// Construct a new context to be used with a single transaction.
    pub fn new() -> Self {
        SaplingProvingContext {
            bsk: Fs::zero(),
            cv_sum: edwards::Point::zero(),
        }
    }

    /// Create the value commitment, re-randomized key, and proof for a Sapling
    /// SpendDescription, while accumulating its value commitment randomness
    /// inside the context for later use.
    pub fn spend_proof(
        &mut self,
        proof_generation_key: ProofGenerationKey<Bls12>,
        diversifier: Diversifier,
        rcm: Fs,
        ar: Fs,
        value: u64,
        anchor: Fr,
        merkle_path: MerklePath<Node>,
        proving_key: &Parameters<Bls12>,
        verifying_key: &PreparedVerifyingKey<Bls12>,
        params: &JubjubBls12,
    ) -> Result<
        (
            Proof<Bls12>,
            edwards::Point<Bls12, Unknown>,
            PublicKey<Bls12>,
        ),
        (),
    > {
        // Initialize secure RNG
        let mut rng = OsRng;

        // We create the randomness of the value commitment
        let rcv = Fs::random(&mut rng);

        // Accumulate the value commitment randomness in the context
        {
            let mut tmp = rcv;
            tmp.add_assign(&self.bsk);

            // Update the context
            self.bsk = tmp;
        }

        // Construct the value commitment
        let value_commitment = ValueCommitment::<Bls12> {
            value,
            randomness: rcv,
        };

        // Construct the viewing key
        let viewing_key = proof_generation_key.to_viewing_key(params);

        // Construct the payment address with the viewing key / diversifier
        let payment_address = viewing_key
            .to_payment_address(diversifier, params)
            .ok_or(())?;

        // This is the result of the re-randomization, we compute it for the caller
        let rk = PublicKey::<Bls12>(proof_generation_key.ak.clone().into()).randomize(
            ar,
            FixedGenerators::SpendingKeyGenerator,
            params,
        );

        // Let's compute the nullifier while we have the position
        let note = Note {
            value,
            g_d: diversifier
                .g_d::<Bls12>(params)
                .expect("was a valid diversifier before"),
            pk_d: payment_address.pk_d().clone(),
            r: rcm,
        };

        let nullifier = note.nf(&viewing_key, merkle_path.position, params);

        // We now have the full witness for our circuit
        let instance = Spend {
            params,
            value_commitment: Some(value_commitment.clone()),
            proof_generation_key: Some(proof_generation_key),
            payment_address: Some(payment_address),
            commitment_randomness: Some(rcm),
            ar: Some(ar),
            auth_path: merkle_path
                .auth_path
                .iter()
                .map(|(node, b)| Some(((*node).into(), *b)))
                .collect(),
            anchor: Some(anchor),
        };

        // Create proof
        let proof =
            create_random_proof(instance, proving_key, &mut rng).expect("proving should not fail");

        // Try to verify the proof:
        // Construct public input for circuit
        let mut public_input = [Fr::zero(); 7];
        {
            let (x, y) = rk.0.to_xy();
            public_input[0] = x;
            public_input[1] = y;
        }
        {
            let (x, y) = value_commitment.cm(params).to_xy();
            public_input[2] = x;
            public_input[3] = y;
        }
        public_input[4] = anchor;

        // Add the nullifier through multiscalar packing
        {
            let nullifier = multipack::bytes_to_bits_le(&nullifier);
            let nullifier = multipack::compute_multipacking::<Bls12>(&nullifier);

            assert_eq!(nullifier.len(), 2);

            public_input[5] = nullifier[0];
            public_input[6] = nullifier[1];
        }

        // Verify the proof
        match verify_proof(verifying_key, &proof, &public_input[..]) {
            // No error, and proof verification successful
            Ok(true) => {}

            // Any other case
            _ => {
                return Err(());
            }
        }

        // Compute value commitment
        let value_commitment: edwards::Point<Bls12, Unknown> = value_commitment.cm(params).into();

        // Accumulate the value commitment in the context
        {
            let mut tmp = value_commitment.clone();
            tmp = tmp.add(&self.cv_sum, params);

            // Update the context
            self.cv_sum = tmp;
        }

        Ok((proof, value_commitment, rk))
    }

    /// Create the value commitment and proof for a Sapling OutputDescription,
    /// while accumulating its value commitment randomness inside the context
    /// for later use.
    pub fn output_proof(
        &mut self,
        esk: Fs,
        payment_address: PaymentAddress<Bls12>,
        rcm: Fs,
        value: u64,
        proving_key: &Parameters<Bls12>,
        params: &JubjubBls12,
    ) -> (Proof<Bls12>, edwards::Point<Bls12, Unknown>) {
        // Initialize secure RNG
        let mut rng = OsRng;

        // We construct ephemeral randomness for the value commitment. This
        // randomness is not given back to the caller, but the synthetic
        // blinding factor `bsk` is accumulated in the context.
        let rcv = Fs::random(&mut rng);

        // Accumulate the value commitment randomness in the context
        {
            let mut tmp = rcv.neg(); // Outputs subtract from the total.
            tmp.add_assign(&self.bsk);

            // Update the context
            self.bsk = tmp;
        }

        // Construct the value commitment for the proof instance
        let value_commitment = ValueCommitment::<Bls12> {
            value,
            randomness: rcv,
        };

        // We now have a full witness for the output proof.
        let instance = Output {
            params,
            value_commitment: Some(value_commitment.clone()),
            payment_address: Some(payment_address.clone()),
            commitment_randomness: Some(rcm),
            esk: Some(esk),
        };

        // Create proof
        let proof =
            create_random_proof(instance, proving_key, &mut rng).expect("proving should not fail");

        // Compute the actual value commitment
        let value_commitment: edwards::Point<Bls12, Unknown> = value_commitment.cm(params).into();

        // Accumulate the value commitment in the context. We do this to check internal consistency.
        {
            let mut tmp = value_commitment.clone();
            tmp = tmp.negate(); // Outputs subtract from the total.
            tmp = tmp.add(&self.cv_sum, params);

            // Update the context
            self.cv_sum = tmp;
        }

        (proof, value_commitment)
    }

    /// Create the bindingSig for a Sapling transaction. All calls to spend_proof()
    /// and output_proof() must be completed before calling this function.
    pub fn binding_sig(
        &self,
        value_balance: Amount,
        sighash: &[u8; 32],
        params: &JubjubBls12,
    ) -> Result<Signature, ()> {
        // Initialize secure RNG
        let mut rng = OsRng;

        // Grab the current `bsk` from the context
        let bsk = PrivateKey::<Bls12>(self.bsk);

        // Grab the `bvk` using DerivePublic.
        let bvk = PublicKey::from_private(&bsk, FixedGenerators::ValueCommitmentRandomness, params);

        // In order to check internal consistency, let's use the accumulated value
        // commitments (as the verifier would) and apply value_balance to compare
        // against our derived bvk.
        {
            // Compute value balance
            let mut value_balance = compute_value_balance(value_balance, params).ok_or(())?;

            // Subtract value_balance from cv_sum to get final bvk
            value_balance = value_balance.negate();
            let mut tmp = self.cv_sum.clone();
            tmp = tmp.add(&value_balance, params);

            // The result should be the same, unless the provided valueBalance is wrong.
            if bvk.0 != tmp {
                return Err(());
            }
        }

        // Construct signature message
        let mut data_to_be_signed = [0u8; 64];
        bvk.0
            .write(&mut data_to_be_signed[0..32])
            .expect("message buffer should be 32 bytes");
        (&mut data_to_be_signed[32..64]).copy_from_slice(&sighash[..]);

        // Sign
        Ok(bsk.sign(
            &data_to_be_signed,
            &mut rng,
            FixedGenerators::ValueCommitmentRandomness,
            params,
        ))
    }
}
