#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
mod mint_provider;
mod queue;
mod queue_provider;
mod runtime_provider;
mod stakes;
mod stakes_provider;
use core::marker::Sized;
use types::{
    account::PublicKey,
    system_contract_errors::pos::{Error, Result},
    AccessRights, TransferredTo, URef, U512,
};
pub use crate::{
    mint_provider::MintProvider, queue::Queue, queue_provider::QueueProvider,
    runtime_provider::RuntimeProvider, stakes::Stakes, stakes_provider::StakesProvider,
};
pub trait ProofOfStake:
    MintProvider + QueueProvider + RuntimeProvider + StakesProvider + Sized
{
    fn bond(&mut self, validator: PublicKey, amount: U512, source: URef) -> Result<()> {
        if amount.is_zero() {
            return Err(Error::BondTooSmall);
        }
        let target = internal::get_bonding_purse(self)?;
        let timestamp = self.get_block_time();
        
        
        self.transfer_purse_to_purse(source, target, amount)
            .map_err(|_| Error::BondTransferFailed)?;
        internal::bond(self, amount, validator, timestamp)?;
        
        let unbonds = internal::step(self, timestamp)?;
        for entry in unbonds {
            let _: TransferredTo = self
                .transfer_purse_to_account(source, entry.validator, entry.amount)
                .map_err(|_| Error::BondTransferFailed)?;
        }
        Ok(())
    }
    fn unbond(&mut self, validator: PublicKey, maybe_amount: Option<U512>) -> Result<()> {
        let pos_purse = internal::get_bonding_purse(self)?;
        let timestamp = self.get_block_time();
        internal::unbond(self, maybe_amount, validator, timestamp)?;
        
        let unbonds = internal::step(self, timestamp)?;
        for entry in unbonds {
            self.transfer_purse_to_account(pos_purse, entry.validator, entry.amount)
                .map_err(|_| Error::UnbondTransferFailed)?;
        }
        Ok(())
    }
    fn get_payment_purse(&self) -> Result<URef> {
        let purse = internal::get_payment_purse(self)?;
        
        Ok(URef::new(purse.addr(), AccessRights::READ_ADD))
    }
    fn set_refund_purse(&mut self, purse: URef) -> Result<()> {
        internal::set_refund(self, purse)
    }
    fn get_refund_purse(&self) -> Result<Option<URef>> {
        
        
        
        let maybe_purse = internal::get_refund_purse(self)?;
        Ok(maybe_purse.map(|p| p.remove_access_rights()))
    }
    fn finalize_payment(&mut self, amount_spent: U512, account: PublicKey) -> Result<()> {
        internal::finalize_payment(self, amount_spent, account)
    }
}
mod internal {
    use alloc::vec::Vec;
    use types::{
        account::PublicKey,
        system_contract_errors::pos::{Error, PurseLookupError, Result},
        BlockTime, Key, Phase, URef, U512,
    };
    use crate::{
        mint_provider::MintProvider, queue::QueueEntry, queue_provider::QueueProvider,
        runtime_provider::RuntimeProvider, stakes_provider::StakesProvider,
    };
    
    const SYSTEM_ACCOUNT: PublicKey = PublicKey::ed25519_from([0u8; 32]);
    
    
    const BONDING_PURSE_KEY: &str = "pos_bonding_purse";
    
    const PAYMENT_PURSE_KEY: &str = "pos_payment_purse";
    
    const REWARDS_PURSE_KEY: &str = "pos_rewards_purse";
    
    
    const REFUND_PURSE_KEY: &str = "pos_refund_purse";
    
    const BOND_DELAY: u64 = 0;
    
    const UNBOND_DELAY: u64 = 0;
    
    const MAX_BOND_LEN: usize = 100;
    
    const MAX_UNBOND_LEN: usize = 1000;
    
    
    pub fn bond<P: QueueProvider + StakesProvider>(
        provider: &mut P,
        amount: U512,
        validator: PublicKey,
        timestamp: BlockTime,
    ) -> Result<()> {
        let mut queue = provider.read_bonding();
        if queue.0.len() >= MAX_BOND_LEN {
            return Err(Error::TooManyEventsInQueue);
        }
        let mut stakes = provider.read()?;
        
        for entry in &queue.0 {
            stakes.bond(&entry.validator, entry.amount);
        }
        stakes.validate_bonding(&validator, amount)?;
        queue.push(validator, amount, timestamp)?;
        provider.write_bonding(queue);
        Ok(())
    }
    
    
    
    pub fn unbond<P: QueueProvider + StakesProvider>(
        provider: &mut P,
        maybe_amount: Option<U512>,
        validator: PublicKey,
        timestamp: BlockTime,
    ) -> Result<()> {
        let mut queue = provider.read_unbonding();
        if queue.0.len() >= MAX_UNBOND_LEN {
            return Err(Error::TooManyEventsInQueue);
        }
        let mut stakes = provider.read()?;
        let payout = stakes.unbond(&validator, maybe_amount)?;
        provider.write(&stakes);
        
        
        
        queue.push(validator, payout, timestamp)?;
        provider.write_unbonding(queue);
        Ok(())
    }
    
    pub fn step<P: QueueProvider + StakesProvider>(
        provider: &mut P,
        timestamp: BlockTime,
    ) -> Result<Vec<QueueEntry>> {
        let mut bonding_queue = provider.read_bonding();
        let mut unbonding_queue = provider.read_unbonding();
        let bonds = bonding_queue.pop_due(timestamp.saturating_sub(BlockTime::new(BOND_DELAY)));
        let unbonds =
            unbonding_queue.pop_due(timestamp.saturating_sub(BlockTime::new(UNBOND_DELAY)));
        if !unbonds.is_empty() {
            provider.write_unbonding(unbonding_queue);
        }
        if !bonds.is_empty() {
            provider.write_bonding(bonding_queue);
            let mut stakes = provider.read()?;
            for entry in bonds {
                stakes.bond(&entry.validator, entry.amount);
            }
            provider.write(&stakes);
        }
        Ok(unbonds)
    }
    
    fn get_purse<R: RuntimeProvider>(
        runtime_provider: &R,
        name: &str,
    ) -> core::result::Result<URef, PurseLookupError> {
        runtime_provider
            .get_key(name)
            .ok_or(PurseLookupError::KeyNotFound)
            .and_then(|key| match key {
                Key::URef(uref) => Ok(uref),
                _ => Err(PurseLookupError::KeyUnexpectedType),
            })
    }
    
    pub fn get_payment_purse<R: RuntimeProvider>(runtime_provider: &R) -> Result<URef> {
        get_purse::<R>(runtime_provider, PAYMENT_PURSE_KEY).map_err(PurseLookupError::payment)
    }
    
    pub fn get_bonding_purse<R: RuntimeProvider>(runtime_provider: &R) -> Result<URef> {
        get_purse::<R>(runtime_provider, BONDING_PURSE_KEY).map_err(PurseLookupError::bonding)
    }
    
    pub fn get_rewards_purse<R: RuntimeProvider>(runtime_provider: &R) -> Result<URef> {
        get_purse::<R>(runtime_provider, REWARDS_PURSE_KEY).map_err(PurseLookupError::rewards)
    }
    
    
    
    pub fn set_refund<R: RuntimeProvider>(runtime_provider: &mut R, purse: URef) -> Result<()> {
        if let Phase::Payment = runtime_provider.get_phase() {
            runtime_provider.put_key(REFUND_PURSE_KEY, Key::URef(purse));
            return Ok(());
        }
        Err(Error::SetRefundPurseCalledOutsidePayment)
    }
    
    pub fn get_refund_purse<R: RuntimeProvider>(runtime_provider: &R) -> Result<Option<URef>> {
        match get_purse::<R>(runtime_provider, REFUND_PURSE_KEY) {
            Ok(uref) => Ok(Some(uref)),
            Err(PurseLookupError::KeyNotFound) => Ok(None),
            Err(PurseLookupError::KeyUnexpectedType) => Err(Error::RefundPurseKeyUnexpectedType),
        }
    }
    
    
    
    
    pub fn finalize_payment<P: MintProvider + RuntimeProvider>(
        provider: &mut P,
        amount_spent: U512,
        account: PublicKey,
    ) -> Result<()> {
        let caller = provider.get_caller();
        if caller != SYSTEM_ACCOUNT {
            return Err(Error::SystemFunctionCalledByUserAccount);
        }
        let payment_purse = get_payment_purse(provider)?;
        let total = match provider.balance(payment_purse) {
            Some(balance) => balance,
            None => return Err(Error::PaymentPurseBalanceNotFound),
        };
        if total < amount_spent {
            return Err(Error::InsufficientPaymentForAmountSpent);
        }
        let refund_amount = total - amount_spent;
        let rewards_purse = get_rewards_purse(provider)?;
        let refund_purse = get_refund_purse(provider)?;
        provider.remove_key(REFUND_PURSE_KEY); 
        
        provider
            .transfer_purse_to_purse(payment_purse, rewards_purse, amount_spent)
            .map_err(|_| Error::FailedTransferToRewardsPurse)?;
        if refund_amount.is_zero() {
            return Ok(());
        }
        
        let refund_purse = match refund_purse {
            Some(uref) => uref,
            None => return refund_to_account::<P>(provider, payment_purse, account, refund_amount),
        };
        
        if provider
            .transfer_purse_to_purse(payment_purse, refund_purse, refund_amount)
            .is_err()
        {
            return refund_to_account::<P>(provider, payment_purse, account, refund_amount);
        }
        Ok(())
    }
    pub fn refund_to_account<M: MintProvider>(
        mint_provider: &mut M,
        payment_purse: URef,
        account: PublicKey,
        amount: U512,
    ) -> Result<()> {
        match mint_provider.transfer_purse_to_account(payment_purse, account, amount) {
            Ok(_) => Ok(()),
            Err(_) => Err(Error::FailedTransferToAccountPurse),
        }
    }
    #[cfg(test)]
    mod tests {
        extern crate std;
        use std::{cell::RefCell, iter, thread_local};
        use types::{account::PublicKey, system_contract_errors::pos::Result, BlockTime, U512};
        use super::{bond, step, unbond, BOND_DELAY, UNBOND_DELAY};
        use crate::{
            queue::Queue, queue_provider::QueueProvider, stakes::Stakes,
            stakes_provider::StakesProvider,
        };
        const KEY1: [u8; 32] = [1; 32];
        const KEY2: [u8; 32] = [2; 32];
        thread_local! {
            static BONDING: RefCell<Queue> = RefCell::new(Queue(Default::default()));
            static UNBONDING: RefCell<Queue> = RefCell::new(Queue(Default::default()));
            static STAKES: RefCell<Stakes> = RefCell::new(
                Stakes(iter::once((PublicKey::ed25519_from(KEY1), U512::from(1_000))).collect())
            );
        }
        struct Provider;
        impl QueueProvider for Provider {
            fn read_bonding(&mut self) -> Queue {
                BONDING.with(|b| b.borrow().clone())
            }
            fn read_unbonding(&mut self) -> Queue {
                UNBONDING.with(|ub| ub.borrow().clone())
            }
            fn write_bonding(&mut self, queue: Queue) {
                BONDING.with(|b| b.replace(queue));
            }
            fn write_unbonding(&mut self, queue: Queue) {
                UNBONDING.with(|ub| ub.replace(queue));
            }
        }
        impl StakesProvider for Provider {
            fn read(&self) -> Result<Stakes> {
                STAKES.with(|s| Ok(s.borrow().clone()))
            }
            fn write(&mut self, stakes: &Stakes) {
                STAKES.with(|s| s.replace(stakes.clone()));
            }
        }
        fn assert_stakes(stakes: &[([u8; 32], usize)]) {
            let expected = Stakes(
                stakes
                    .iter()
                    .map(|(key, amount)| (PublicKey::ed25519_from(*key), U512::from(*amount)))
                    .collect(),
            );
            assert_eq!(Ok(expected), Provider.read());
        }
        #[test]
        fn test_bond_step_unbond() {
            let mut provider = Provider;
            bond(
                &mut provider,
                U512::from(500),
                PublicKey::ed25519_from(KEY2),
                BlockTime::new(1),
            )
            .expect("bond validator 2");
            
            assert_stakes(&[(KEY1, 1_000)]);
            step(&mut provider, BlockTime::new(BOND_DELAY)).expect("step 1");
            assert_stakes(&[(KEY1, 1_000)]);
            step(&mut provider, BlockTime::new(1 + BOND_DELAY)).expect("step 2");
            assert_stakes(&[(KEY1, 1_000), (KEY2, 500)]);
            unbond::<Provider>(
                &mut provider,
                Some(U512::from(500)),
                PublicKey::ed25519_from(KEY1),
                BlockTime::new(2),
            )
            .expect("partly unbond validator 1");
            
            assert_stakes(&[(KEY1, 500), (KEY2, 500)]);
            step::<Provider>(&mut provider, BlockTime::new(2 + UNBOND_DELAY)).expect("step 3");
            assert_stakes(&[(KEY1, 500), (KEY2, 500)]);
        }
    }
}