// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
// This file is either (a) original to Subzero Labs, Inc. or (b) derived from the Anza codebase and modified by Subzero Labs, Inc.

//! Utility functions
use rialo_s_clock::Epoch;
#[cfg(feature = "bincode")]
use {
    rialo_s_cpi::{get_return_data, invoke_unchecked},
    rialo_s_program_error::ProgramError,
};

use crate::MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION;

/// Helper function for programs to call [`GetMinimumDelegation`] and then fetch the return data
///
/// This fn handles performing the CPI to call the [`GetMinimumDelegation`] function, and then
/// calls [`get_return_data()`] to fetch the return data.
///
/// [`GetMinimumDelegation`]: crate::instruction::StakeInstruction::GetMinimumDelegation
/// [`get_return_data()`]: rialo_s_cpi::get_return_data
#[cfg(feature = "bincode")]
pub fn get_minimum_delegation() -> Result<u64, ProgramError> {
    let instruction = crate::instruction::get_minimum_delegation();
    invoke_unchecked(&instruction, &[])?;
    get_minimum_delegation_return_data()
}

/// Helper function for programs to get the return data after calling [`GetMinimumDelegation`]
///
/// This fn handles calling [`get_return_data()`], ensures the result is from the correct
/// program, and returns the correct type.
///
/// [`GetMinimumDelegation`]: crate::instruction::StakeInstruction::GetMinimumDelegation
/// [`get_return_data()`]: rialo_s_cpi::get_return_data
#[cfg(feature = "bincode")]
fn get_minimum_delegation_return_data() -> Result<u64, ProgramError> {
    get_return_data()
        .ok_or(ProgramError::InvalidInstructionData)
        .and_then(|(program_id, return_data)| {
            (program_id == crate::program::id())
                .then_some(return_data)
                .ok_or(ProgramError::IncorrectProgramId)
        })
        .and_then(|return_data| {
            return_data
                .try_into()
                .or(Err(ProgramError::InvalidInstructionData))
        })
        .map(u64::from_le_bytes)
}

/// Check if the provided `epoch_credits` demonstrate active voting over the previous
/// [`MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`].
pub fn acceptable_reference_epoch_credits(
    epoch_credits: &[(Epoch, u64, u64)],
    current_epoch: Epoch,
) -> bool {
    if let Some(epoch_index) = epoch_credits
        .len()
        .checked_sub(MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION)
    {
        let mut epoch = current_epoch;
        for (vote_epoch, ..) in epoch_credits[epoch_index..].iter().rev() {
            if *vote_epoch != epoch {
                return false;
            }
            epoch = epoch.saturating_sub(1);
        }
        true
    } else {
        false
    }
}

/// Check if the provided `epoch_credits` demonstrate delinquency over the previous
/// [`MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`].
pub fn eligible_for_deactivate_delinquent(
    epoch_credits: &[(Epoch, u64, u64)],
    current_epoch: Epoch,
) -> bool {
    match epoch_credits.last() {
        None => true,
        Some((epoch, ..)) => {
            if let Some(minimum_epoch) =
                current_epoch.checked_sub(MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch)
            {
                *epoch <= minimum_epoch
            } else {
                false
            }
        }
    }
}

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

    #[test]
    fn test_acceptable_reference_epoch_credits() {
        let epoch_credits = [];
        assert!(!acceptable_reference_epoch_credits(&epoch_credits, 0));

        let epoch_credits = [(0, 42, 42), (1, 42, 42), (2, 42, 42), (3, 42, 42)];
        assert!(!acceptable_reference_epoch_credits(&epoch_credits, 3));

        let epoch_credits = [
            (0, 42, 42),
            (1, 42, 42),
            (2, 42, 42),
            (3, 42, 42),
            (4, 42, 42),
        ];
        assert!(!acceptable_reference_epoch_credits(&epoch_credits, 3));
        assert!(acceptable_reference_epoch_credits(&epoch_credits, 4));

        let epoch_credits = [
            (1, 42, 42),
            (2, 42, 42),
            (3, 42, 42),
            (4, 42, 42),
            (5, 42, 42),
        ];
        assert!(acceptable_reference_epoch_credits(&epoch_credits, 5));

        let epoch_credits = [
            (0, 42, 42),
            (2, 42, 42),
            (3, 42, 42),
            (4, 42, 42),
            (5, 42, 42),
        ];
        assert!(!acceptable_reference_epoch_credits(&epoch_credits, 5));
    }

    #[test]
    fn test_eligible_for_deactivate_delinquent() {
        let epoch_credits = [];
        assert!(eligible_for_deactivate_delinquent(&epoch_credits, 42));

        let epoch_credits = [(0, 42, 42)];
        assert!(!eligible_for_deactivate_delinquent(&epoch_credits, 0));

        let epoch_credits = [(0, 42, 42)];
        assert!(!eligible_for_deactivate_delinquent(
            &epoch_credits,
            MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch - 1
        ));
        assert!(eligible_for_deactivate_delinquent(
            &epoch_credits,
            MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch
        ));

        let epoch_credits = [(100, 42, 42)];
        assert!(!eligible_for_deactivate_delinquent(
            &epoch_credits,
            100 + MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch - 1
        ));
        assert!(eligible_for_deactivate_delinquent(
            &epoch_credits,
            100 + MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch
        ));
    }
}
