//! Arcis implementation of <https://github.com/solana-program/zk-elgamal-proof/blob/main/zk-sdk/src/encryption/pedersen.rs>

use crate::{
    core::global_value::{curve_value::CurveValue, value::FieldValue},
    traits::Reveal,
    utils::field::ScalarField,
};
use std::{
    ops::{Add, Mul, Sub},
    sync::LazyLock,
};
use zk_elgamal_proof::encryption::pedersen::H;

/// Algorithm handle for the Pedersen commitment scheme.
pub struct Pedersen;
impl Pedersen {
    /// On input a message (numeric amount), the function returns a Pedersen commitment of the
    /// message and the corresponding opening.
    ///
    /// This function is randomized. It internally samples a Pedersen opening.
    #[allow(clippy::new_ret_no_self)]
    pub fn new(amount: FieldValue<ScalarField>) -> (PedersenCommitment, PedersenOpening) {
        let opening = PedersenOpening::new_rand();
        let commitment = Pedersen::with(amount, &opening);

        (commitment, opening)
    }

    /// On input a message (numeric amount) and a Pedersen opening, the function returns the
    /// corresponding Pedersen commitment.
    ///
    /// This function is deterministic.
    pub fn with(amount: FieldValue<ScalarField>, opening: &PedersenOpening) -> PedersenCommitment {
        let x = amount;
        let r = opening.get_scalar();

        PedersenCommitment(
            CurveValue::multiscalar_mul(
                vec![x, *r],
                vec![
                    CurveValue::generator(),
                    CurveValue::from(*LazyLock::force(&H)),
                ],
            )
            .reveal(),
        )
    }

    /// On input a message, the function returns a Pedersen commitment with zero as the opening.
    ///
    /// This function is deterministic. The commitment is binding, but depending on the size of
    /// amount it is not hiding.
    pub fn encode(amount: FieldValue<ScalarField>) -> PedersenCommitment {
        PedersenCommitment((amount * CurveValue::generator()).reveal())
    }
}

/// Pedersen opening type.
#[derive(Clone, Debug)]
pub struct PedersenOpening(FieldValue<ScalarField>);

impl PedersenOpening {
    pub fn new(scalar: FieldValue<ScalarField>) -> Self {
        assert!(!scalar.is_plaintext());
        Self(scalar)
    }

    pub fn new_rand() -> Self {
        PedersenOpening(FieldValue::<ScalarField>::random())
    }

    pub fn get_scalar(&self) -> &FieldValue<ScalarField> {
        &self.0
    }
}

impl Add<&PedersenOpening> for &PedersenOpening {
    type Output = PedersenOpening;

    fn add(self, opening: &PedersenOpening) -> PedersenOpening {
        PedersenOpening(self.0 + opening.0)
    }
}

impl Mul<FieldValue<ScalarField>> for &PedersenOpening {
    type Output = PedersenOpening;

    fn mul(self, scalar: FieldValue<ScalarField>) -> PedersenOpening {
        PedersenOpening(self.0 * scalar)
    }
}

/// Pedersen commitment type.
#[derive(Clone, Copy, Debug)]
pub struct PedersenCommitment(CurveValue);

impl PedersenCommitment {
    pub fn new(point: CurveValue) -> Self {
        assert!(point.is_plaintext());
        Self(point)
    }

    pub fn get_point(&self) -> &CurveValue {
        &self.0
    }
}

impl Add for PedersenCommitment {
    type Output = PedersenCommitment;

    fn add(self, commitment: PedersenCommitment) -> Self::Output {
        PedersenCommitment(self.0 + commitment.0)
    }
}

impl Sub for PedersenCommitment {
    type Output = PedersenCommitment;

    fn sub(self, commitment: PedersenCommitment) -> Self::Output {
        PedersenCommitment(self.0 - commitment.0)
    }
}

impl Mul<PedersenCommitment> for FieldValue<ScalarField> {
    type Output = PedersenCommitment;

    fn mul(self, rhs: PedersenCommitment) -> Self::Output {
        PedersenCommitment(self * rhs.0)
    }
}
