//! Calculation of transaction fees.
#![cfg_attr(feature = "frozen-abi", feature(min_specialization))]
#![allow(clippy::arithmetic_side_effects)]
#![no_std]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use log::*;
#[cfg(feature = "frozen-abi")]
extern crate std;

#[repr(C)]
#[cfg_attr(feature = "frozen-abi", derive(rialo_frozen_abi_macro::AbiExample))]
#[cfg_attr(
    feature = "serde",
    derive(serde_derive::Serialize, serde_derive::Deserialize)
)]
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct FeeCalculator {
    /// The current cost of a signature.
    ///
    /// This amount may increase/decrease over time based on cluster processing
    /// load.
    pub kelvins_per_signature: u64,
}

impl FeeCalculator {
    pub fn new(kelvins_per_signature: u64) -> Self {
        Self {
            kelvins_per_signature,
        }
    }
}

#[cfg_attr(feature = "frozen-abi", derive(rialo_frozen_abi_macro::AbiExample))]
#[cfg_attr(
    feature = "serde",
    derive(serde_derive::Serialize, serde_derive::Deserialize)
)]
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct FeeRateGovernor {
    // The current cost of a signature  This amount may increase/decrease over time based on
    // cluster processing load.
    #[cfg_attr(feature = "serde", serde(skip))]
    pub kelvins_per_signature: u64,

    // The target cost of a signature when the cluster is operating around target_signatures_per_slot
    // signatures
    pub target_kelvins_per_signature: u64,

    // Used to estimate the desired processing capacity of the cluster.  As the signatures for
    // recent slots are fewer/greater than this value, kelvins_per_signature will decrease/increase
    // for the next slot.  A value of 0 disables kelvins_per_signature fee adjustments
    pub target_signatures_per_slot: u64,

    pub min_kelvins_per_signature: u64,
    pub max_kelvins_per_signature: u64,

    // What portion of collected fees are to be destroyed, as a fraction of u8::MAX
    pub burn_percent: u8,
}

pub const DEFAULT_TARGET_KELVINS_PER_SIGNATURE: u64 = 10_000;
const DEFAULT_MS_PER_SLOT: u64 = 400;
#[cfg(test)]
static_assertions::const_assert_eq!(DEFAULT_MS_PER_SLOT, rialo_s_clock::DEFAULT_MS_PER_SLOT);
pub const DEFAULT_TARGET_SIGNATURES_PER_SLOT: u64 = 50 * DEFAULT_MS_PER_SLOT;

// Percentage of tx fees to burn
pub const DEFAULT_BURN_PERCENT: u8 = 50;

impl Default for FeeRateGovernor {
    fn default() -> Self {
        Self {
            kelvins_per_signature: 0,
            target_kelvins_per_signature: DEFAULT_TARGET_KELVINS_PER_SIGNATURE,
            target_signatures_per_slot: DEFAULT_TARGET_SIGNATURES_PER_SLOT,
            min_kelvins_per_signature: 0,
            max_kelvins_per_signature: 0,
            burn_percent: DEFAULT_BURN_PERCENT,
        }
    }
}

impl FeeRateGovernor {
    pub fn new(target_kelvins_per_signature: u64, target_signatures_per_slot: u64) -> Self {
        let base_fee_rate_governor = Self {
            target_kelvins_per_signature,
            kelvins_per_signature: target_kelvins_per_signature,
            target_signatures_per_slot,
            ..FeeRateGovernor::default()
        };

        Self::new_derived(&base_fee_rate_governor, 0)
    }

    pub fn new_derived(
        base_fee_rate_governor: &FeeRateGovernor,
        latest_signatures_per_slot: u64,
    ) -> Self {
        let mut me = base_fee_rate_governor.clone();

        if me.target_signatures_per_slot > 0 {
            // kelvins_per_signature can range from 50% to 1000% of
            // target_kelvins_per_signature
            me.min_kelvins_per_signature = core::cmp::max(1, me.target_kelvins_per_signature / 2);
            me.max_kelvins_per_signature = me.target_kelvins_per_signature * 10;

            // What the cluster should charge at `latest_signatures_per_slot`
            let desired_kelvins_per_signature = me.max_kelvins_per_signature.min(
                me.min_kelvins_per_signature.max(
                    (me.target_kelvins_per_signature
                        * core::cmp::min(latest_signatures_per_slot, u32::MAX as u64))
                    .checked_div(me.target_signatures_per_slot)
                    .unwrap_or(0),
                ),
            );

            trace!(
                "desired_kelvins_per_signature: {}",
                desired_kelvins_per_signature
            );

            let gap = desired_kelvins_per_signature as i64
                - base_fee_rate_governor.kelvins_per_signature as i64;

            if gap == 0 {
                me.kelvins_per_signature = desired_kelvins_per_signature;
            } else {
                // Adjust fee by 5% of target_kelvins_per_signature to produce a smooth
                // increase/decrease in fees over time.
                let gap_adjust =
                    core::cmp::max(1, me.target_kelvins_per_signature / 20) as i64 * gap.signum();

                trace!(
                    "kelvins_per_signature gap is {}, adjusting by {}",
                    gap,
                    gap_adjust
                );

                me.kelvins_per_signature =
                    me.max_kelvins_per_signature
                        .min(me.min_kelvins_per_signature.max(
                            (base_fee_rate_governor.kelvins_per_signature as i64 + gap_adjust)
                                as u64,
                        ));
            }
        } else {
            me.kelvins_per_signature = base_fee_rate_governor.target_kelvins_per_signature;
            me.min_kelvins_per_signature = me.target_kelvins_per_signature;
            me.max_kelvins_per_signature = me.target_kelvins_per_signature;
        }
        debug!(
            "new_derived(): kelvins_per_signature: {}",
            me.kelvins_per_signature
        );
        me
    }

    pub fn clone_with_kelvins_per_signature(&self, kelvins_per_signature: u64) -> Self {
        Self {
            kelvins_per_signature,
            ..*self
        }
    }

    /// calculate unburned fee from a fee total, returns (unburned, burned)
    pub fn burn(&self, fees: u64) -> (u64, u64) {
        let burned = fees * u64::from(self.burn_percent) / 100;
        (fees - burned, burned)
    }

    /// create a FeeCalculator based on current cluster signature throughput
    pub fn create_fee_calculator(&self) -> FeeCalculator {
        FeeCalculator::new(self.kelvins_per_signature)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_fee_rate_governor_burn() {
        let mut fee_rate_governor = FeeRateGovernor::default();
        assert_eq!(fee_rate_governor.burn(2), (1, 1));

        fee_rate_governor.burn_percent = 0;
        assert_eq!(fee_rate_governor.burn(2), (2, 0));

        fee_rate_governor.burn_percent = 100;
        assert_eq!(fee_rate_governor.burn(2), (0, 2));
    }

    #[test]
    fn test_fee_rate_governor_derived_default() {
        rialo_s_logger::setup();

        let f0 = FeeRateGovernor::default();
        assert_eq!(
            f0.target_signatures_per_slot,
            DEFAULT_TARGET_SIGNATURES_PER_SLOT
        );
        assert_eq!(
            f0.target_kelvins_per_signature,
            DEFAULT_TARGET_KELVINS_PER_SIGNATURE
        );
        assert_eq!(f0.kelvins_per_signature, 0);

        let f1 = FeeRateGovernor::new_derived(&f0, DEFAULT_TARGET_SIGNATURES_PER_SLOT);
        assert_eq!(
            f1.target_signatures_per_slot,
            DEFAULT_TARGET_SIGNATURES_PER_SLOT
        );
        assert_eq!(
            f1.target_kelvins_per_signature,
            DEFAULT_TARGET_KELVINS_PER_SIGNATURE
        );
        assert_eq!(
            f1.kelvins_per_signature,
            DEFAULT_TARGET_KELVINS_PER_SIGNATURE / 2
        ); // min
    }

    #[test]
    fn test_fee_rate_governor_derived_adjust() {
        rialo_s_logger::setup();

        let mut f = FeeRateGovernor {
            target_kelvins_per_signature: 100,
            target_signatures_per_slot: 100,
            ..FeeRateGovernor::default()
        };
        f = FeeRateGovernor::new_derived(&f, 0);

        // Ramp fees up
        let mut count = 0;
        loop {
            let last_kelvins_per_signature = f.kelvins_per_signature;

            f = FeeRateGovernor::new_derived(&f, u64::MAX);
            info!("[up] f.kelvins_per_signature={}", f.kelvins_per_signature);

            // some maximum target reached
            if f.kelvins_per_signature == last_kelvins_per_signature {
                break;
            }
            // shouldn't take more than 1000 steps to get to minimum
            assert!(count < 1000);
            count += 1;
        }

        // Ramp fees down
        let mut count = 0;
        loop {
            let last_kelvins_per_signature = f.kelvins_per_signature;
            f = FeeRateGovernor::new_derived(&f, 0);

            info!("[down] f.kelvins_per_signature={}", f.kelvins_per_signature);

            // some minimum target reached
            if f.kelvins_per_signature == last_kelvins_per_signature {
                break;
            }

            // shouldn't take more than 1000 steps to get to minimum
            assert!(count < 1000);
            count += 1;
        }

        // Arrive at target rate
        let mut count = 0;
        while f.kelvins_per_signature != f.target_kelvins_per_signature {
            f = FeeRateGovernor::new_derived(&f, f.target_signatures_per_slot);
            info!(
                "[target] f.kelvins_per_signature={}",
                f.kelvins_per_signature
            );
            // shouldn't take more than 100 steps to get to target
            assert!(count < 100);
            count += 1;
        }
    }
}
