// Copyright 2022 The Tari Project
// SPDX-License-Identifier: BSD-3-Clause
//   Modified from:
//     Copyright (c) 2018 Chain, Inc.
//     SPDX-License-Identifier: MIT

#![allow(missing_docs)]

#[macro_use]
extern crate criterion;

use criterion::{Criterion, SamplingMode};
use curve25519_dalek::scalar::Scalar;
use rand_chacha::ChaCha12Rng;
use rand_core::{Rng, SeedableRng};
use tari_bulletproofs_plus::{
    commitment_opening::CommitmentOpening,
    generators::pedersen_gens::ExtensionDegree,
    protocols::scalar_protocol::ScalarProtocol,
    range_parameters::RangeParameters,
    range_proof::{RangeProof, VerifyAction},
    range_statement::RangeStatement,
    range_witness::RangeWitness,
    ristretto,
    ristretto::RistrettoRangeProof,
};
use tari_merlin::Transcript;

// Reduced spectrum of tests for the sake of CI bench tests
static AGGREGATION_SIZES: [usize; 4] = [1, 2, 4, 8];
static BATCHED_SIZES: [usize; 4] = [1, 2, 4, 8];
static BIT_LENGTHS: [usize; 3] = [2, 4, 8];
static EXTENSION_DEGREE: [ExtensionDegree; 1] = [ExtensionDegree::DefaultPedersen];
static EXTRACT_MASKS: [VerifyAction; 1] = [VerifyAction::VerifyOnly];
// To do a full spectrum of tests, use these constants instead
// static AGGREGATION_SIZES: [usize; 6] = [1, 2, 4, 8, 16, 32];
// static BATCHED_SIZES: [usize; 9] = [1, 2, 4, 8, 16, 32, 64, 128, 256];
// static BIT_LENGTHS: [usize; 3] = [8, 16, 32];
// static EXTENSION_DEGREE: [ExtensionDegree; 3] = [ExtensionDegree::Zero, ExtensionDegree::Two, ExtensionDegree::Four];
// static EXTRACT_MASKS: [VerifyAction; 2] =  [VerifyAction::VerifyOnly, VerifyAction::RecoverOnly];

#[allow(clippy::arithmetic_side_effects)]
fn create_aggregated_rangeproof_helper(bit_length: usize, extension_degree: ExtensionDegree, c: &mut Criterion) {
    let mut group = c.benchmark_group("range_proof_creation");
    group.sampling_mode(SamplingMode::Flat);

    let mut rng = ChaCha12Rng::seed_from_u64(8675309); // for testing only!

    let transcript_label: &'static str = "BatchedRangeProofTest";
    #[allow(clippy::cast_possible_truncation)]
    let value_max = (1u128 << (bit_length - 1)) as u64;

    for aggregation_factor in AGGREGATION_SIZES {
        let label = format!(
            "Agg {}-bit BP+ create agg factor {} degree {:?}",
            bit_length, aggregation_factor, extension_degree
        );
        group.bench_function(&label, |b| {
            // 1. Generators
            let generators = RangeParameters::init(
                bit_length,
                aggregation_factor,
                ristretto::create_pedersen_gens_with_extension_degree(extension_degree),
            )
            .unwrap();

            // 2. Create witness data
            let mut commitments = vec![];
            let mut minimum_values = vec![];
            let mut openings = vec![];
            for _ in 0..aggregation_factor {
                let value = rng.next_u64() % value_max; // introduces bias, but that's fine for testing
                minimum_values.push(Some(value / 3));
                let blindings = vec![Scalar::random_not_zero(&mut rng); extension_degree as usize];
                commitments.push(
                    generators
                        .pc_gens()
                        .commit(&Scalar::from(value), blindings.as_slice())
                        .unwrap(),
                );
                openings.push(CommitmentOpening::new(value, blindings.clone()));
            }
            let witness = RangeWitness::init(openings).unwrap();

            // 3. Generate the statement
            let seed_nonce = if aggregation_factor == 1 {
                Some(Scalar::random_not_zero(&mut rng))
            } else {
                None
            };
            let statement =
                RangeStatement::init(generators, commitments.clone(), minimum_values.clone(), seed_nonce).unwrap();

            // Benchmark this code
            b.iter(|| {
                // 4. Create the aggregated proof
                let _proof = RistrettoRangeProof::prove_with_rng(
                    &mut Transcript::new(transcript_label.as_bytes()),
                    &statement,
                    &witness,
                    &mut rng,
                );
            })
        });
    }
    group.finish();
}

fn create_aggregated_rangeproof_n_small(c: &mut Criterion) {
    for bit_length in BIT_LENGTHS {
        create_aggregated_rangeproof_helper(bit_length, ExtensionDegree::DefaultPedersen, c);
    }
}

fn create_aggregated_rangeproof_n_64(c: &mut Criterion) {
    for extension_degree in &EXTENSION_DEGREE {
        create_aggregated_rangeproof_helper(64, *extension_degree, c);
    }
}

#[allow(clippy::arithmetic_side_effects)]
fn verify_aggregated_rangeproof_helper(bit_length: usize, extension_degree: ExtensionDegree, c: &mut Criterion) {
    let mut group = c.benchmark_group("range_proof_verification");
    group.sampling_mode(SamplingMode::Flat);

    let mut rng = ChaCha12Rng::seed_from_u64(8675309); // for testing only!

    let transcript_label: &'static str = "BatchedRangeProofTest";
    #[allow(clippy::cast_possible_truncation)]
    let value_max = (1u128 << (bit_length - 1)) as u64;

    for aggregation_factor in AGGREGATION_SIZES {
        let pederson_gens = ristretto::create_pedersen_gens_with_extension_degree(extension_degree);
        let label = format!(
            "Agg {}-bit BP+ verify agg factor {} degree {:?}",
            bit_length, aggregation_factor, extension_degree
        );
        group.bench_function(&label, |b| {
            // 0. Batch data
            let mut statements = vec![];
            let mut proofs = vec![];
            let mut transcripts = vec![];

            // 1. Generators
            let generators = RangeParameters::init(bit_length, aggregation_factor, pederson_gens.clone()).unwrap();

            // 2. Create witness data
            let mut commitments = vec![];
            let mut minimum_values = vec![];
            let mut openings = vec![];
            for _ in 0..aggregation_factor {
                let value = rng.next_u64() % value_max; // introduces bias, but that's fine for testing
                minimum_values.push(Some(value / 3));
                let blindings = vec![Scalar::random_not_zero(&mut rng); extension_degree as usize];
                commitments.push(
                    generators
                        .pc_gens()
                        .commit(&Scalar::from(value), blindings.as_slice())
                        .unwrap(),
                );
                openings.push(CommitmentOpening::new(value, blindings.clone()));
            }
            let witness = RangeWitness::init(openings).unwrap();

            // 3. Generate the statement
            let seed_nonce = if aggregation_factor == 1 {
                Some(Scalar::random_not_zero(&mut rng))
            } else {
                None
            };
            let statement =
                RangeStatement::init(generators, commitments.clone(), minimum_values.clone(), seed_nonce).unwrap();
            statements.push(statement.clone());
            let mut transcript = Transcript::new(transcript_label.as_bytes());
            transcripts.push(transcript.clone());

            // 4. Create the proof
            let proof = RistrettoRangeProof::prove_with_rng(&mut transcript, &statement, &witness, &mut rng).unwrap();
            proofs.push(proof);

            // Benchmark this code
            b.iter(|| {
                // 5. Verify the aggregated proof
                let _masks =
                    RangeProof::verify_batch(&mut transcripts.clone(), &statements, &proofs, VerifyAction::VerifyOnly)
                        .unwrap();
            });
        });
    }
    group.finish();
}

fn verify_aggregated_rangeproof_n_small(c: &mut Criterion) {
    for bit_length in BIT_LENGTHS {
        verify_aggregated_rangeproof_helper(bit_length, ExtensionDegree::DefaultPedersen, c);
    }
}

fn verify_aggregated_rangeproof_n_64(c: &mut Criterion) {
    for extension_degree in &EXTENSION_DEGREE {
        verify_aggregated_rangeproof_helper(64, *extension_degree, c);
    }
}

#[allow(clippy::arithmetic_side_effects)]
fn verify_batched_rangeproofs_helper(bit_length: usize, extension_degree: ExtensionDegree, c: &mut Criterion) {
    let mut group = c.benchmark_group("batched_range_proof_verification");
    group.sampling_mode(SamplingMode::Flat);

    let mut rng = ChaCha12Rng::seed_from_u64(8675309); // for testing only!

    let transcript_label: &'static str = "BatchedRangeProofTest";
    #[allow(clippy::cast_possible_truncation)]
    let value_max = (1u128 << (bit_length - 1)) as u64;

    for extract_masks in EXTRACT_MASKS {
        for number_of_range_proofs in BATCHED_SIZES {
            let label = format!(
                "Batched {}-bit BP+ verify {} deg {:?} masks {:?}",
                bit_length, number_of_range_proofs, extension_degree, extract_masks
            );

            // Generators
            let pc_gens = ristretto::create_pedersen_gens_with_extension_degree(extension_degree);
            let generators = RangeParameters::init(bit_length, 1, pc_gens).unwrap();

            group.bench_function(&label, |b| {
                // Batch data
                let mut statements = vec![];
                let mut proofs = vec![];
                let mut transcripts = vec![];

                for _ in 0..number_of_range_proofs {
                    // Witness data
                    let mut openings = vec![];
                    let value = rng.next_u64() % value_max; // introduces bias, but that's fine for testing
                    let blindings = vec![Scalar::random_not_zero(&mut rng); extension_degree as usize];
                    openings.push(CommitmentOpening::new(value, blindings.clone()));
                    let witness = RangeWitness::init(openings).unwrap();

                    // Statement data
                    let seed_nonce = Some(Scalar::random_not_zero(&mut rng));
                    let statement = RangeStatement::init(
                        generators.clone(),
                        vec![
                            generators
                                .pc_gens()
                                .commit(&Scalar::from(value), blindings.as_slice())
                                .unwrap(),
                        ],
                        vec![Some(value / 3)],
                        seed_nonce,
                    )
                    .unwrap();
                    statements.push(statement.clone());
                    let mut transcript = Transcript::new(transcript_label.as_bytes());
                    transcripts.push(transcript.clone());

                    // Proof
                    let proof =
                        RistrettoRangeProof::prove_with_rng(&mut transcript, &statement, &witness, &mut rng).unwrap();
                    proofs.push(proof);
                }

                // Benchmark this code
                b.iter(|| {
                    // Verify the entire batch of proofs
                    match extract_masks {
                        VerifyAction::VerifyOnly => {
                            let _masks = RangeProof::verify_batch(
                                &mut transcripts.clone(),
                                &statements,
                                &proofs,
                                VerifyAction::VerifyOnly,
                            )
                            .unwrap();
                        },
                        VerifyAction::RecoverOnly => {
                            let _masks = RangeProof::verify_batch(
                                &mut transcripts.clone(),
                                &statements,
                                &proofs,
                                VerifyAction::RecoverOnly,
                            )
                            .unwrap();
                        },
                        _ => {},
                    }
                });
            });
        }
    }
    group.finish();
}

fn verify_batched_rangeproof_n_64(c: &mut Criterion) {
    for extension_degree in &EXTENSION_DEGREE {
        verify_batched_rangeproofs_helper(64, *extension_degree, c);
    }
}

criterion_group! {
    name = create_rp;
    config = Criterion::default();
    targets =
    create_aggregated_rangeproof_n_small,
    create_aggregated_rangeproof_n_64,
}

criterion_group! {
    name = verify_rp;
    config = Criterion::default();
    targets =
    verify_aggregated_rangeproof_n_small,
    verify_aggregated_rangeproof_n_64,
}

criterion_group! {
    name = verify_batched_rp;
    config = Criterion::default();
    targets =
    verify_batched_rangeproof_n_64,
}

criterion_main!(create_rp, verify_rp, verify_batched_rp);
