#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
pub mod weights;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub use cfg_traits::rewards::{
	AccountRewards, CurrencyGroupChange, DistributedRewards, GroupRewards,
};
use frame_support::{
	pallet_prelude::*,
	traits::{
		tokens::{AssetId, Balance},
		Time,
	},
	DefaultNoBound,
};
pub use frame_support::{
	storage::{bounded_btree_map::BoundedBTreeMap, transactional},
	transactional,
};
use frame_system::pallet_prelude::*;
use num_traits::sign::Unsigned;
pub use pallet::*;
use sp_runtime::{
	traits::{EnsureAdd, Zero},
	FixedPointOperand,
};
use sp_std::mem;
pub use weights::WeightInfo;
#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, RuntimeDebugNoBound)]
#[scale_info(skip_type_params(T))]
pub struct EpochData<T: Config> {
	duration: MomentOf<T>,
	reward: T::Balance,
	weights: BoundedBTreeMap<T::GroupId, T::Weight, T::MaxGroups>,
}
impl<T: Config> Default for EpochData<T> {
	fn default() -> Self {
		Self {
			duration: T::InitialEpochDuration::get(),
			reward: T::Balance::zero(),
			weights: BoundedBTreeMap::default(),
		}
	}
}
impl<T: Config> Clone for EpochData<T> {
	fn clone(&self) -> Self {
		Self {
			duration: self.duration,
			reward: self.reward,
			weights: self.weights.clone(),
		}
	}
}
#[derive(
	PartialEq, Clone, DefaultNoBound, Encode, Decode, TypeInfo, MaxEncodedLen, RuntimeDebugNoBound,
)]
#[scale_info(skip_type_params(T))]
pub struct EpochChanges<T: Config> {
	duration: Option<MomentOf<T>>,
	reward: Option<T::Balance>,
	weights: BoundedBTreeMap<T::GroupId, T::Weight, T::MaxChangesPerEpoch>,
	currencies: BoundedBTreeMap<T::CurrencyId, T::GroupId, T::MaxChangesPerEpoch>,
}
pub type MomentOf<T> = <<T as Config>::Timer as Time>::Moment;
#[frame_support::pallet]
pub mod pallet {
	use super::*;
	#[pallet::config]
	pub trait Config: frame_system::Config {
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
		type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
		type Balance: Balance + MaxEncodedLen + FixedPointOperand;
		type CurrencyId: AssetId + MaxEncodedLen + Clone + Ord;
		type GroupId: Parameter + MaxEncodedLen + Ord + Copy;
		type Weight: Parameter + MaxEncodedLen + EnsureAdd + Unsigned + FixedPointOperand + Default;
		type Rewards: GroupRewards<Balance = Self::Balance, GroupId = Self::GroupId>
			+ AccountRewards<Self::AccountId, Balance = Self::Balance, CurrencyId = Self::CurrencyId>
			+ CurrencyGroupChange<GroupId = Self::GroupId, CurrencyId = Self::CurrencyId>
			+ DistributedRewards<Balance = Self::Balance, GroupId = Self::GroupId>;
		type Timer: Time;
		#[pallet::constant]
		type MaxGroups: Get<u32> + TypeInfo;
		#[pallet::constant]
		type MaxChangesPerEpoch: Get<u32> + TypeInfo + sp_std::fmt::Debug + Clone + PartialEq;
		#[pallet::constant]
		type InitialEpochDuration: Get<MomentOf<Self>>;
		type WeightInfo: WeightInfo;
	}
	#[pallet::pallet]
	pub struct Pallet<T>(_);
	#[pallet::storage]
	pub(super) type EndOfEpoch<T: Config> = StorageValue<_, MomentOf<T>, ValueQuery>;
	#[pallet::storage]
	pub(super) type ActiveEpochData<T: Config> = StorageValue<_, EpochData<T>, ValueQuery>;
	#[pallet::storage]
	pub(super) type NextEpochChanges<T: Config> = StorageValue<_, EpochChanges<T>, ValueQuery>;
	#[pallet::event]
	#[pallet::generate_deposit(pub(super) fn deposit_event)]
	pub enum Event<T: Config> {
		NewEpoch {
			ends_on: MomentOf<T>,
			reward: T::Balance,
			last_changes: EpochChanges<T>,
		},
	}
	#[pallet::error]
	pub enum Error<T> {
		MaxChangesPerEpochReached,
	}
	#[derive(Default)]
	pub struct ChangeCounter {
		groups: u32,
		weights: u32,
		currencies: u32,
	}
	#[pallet::hooks]
	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
		fn on_initialize(_: BlockNumberFor<T>) -> Weight {
			let now = T::Timer::now();
			if now < EndOfEpoch::<T>::get() {
				return T::DbWeight::get().reads(1);
			}
			let mut counter = ChangeCounter::default();
			transactional::with_storage_layer(|| -> DispatchResult {
				let (epoch_data, last_changes) = Self::apply_epoch_changes(&mut counter)?;
				let ends_on = now.ensure_add(epoch_data.duration)?;
				EndOfEpoch::<T>::set(ends_on);
				Self::deposit_event(Event::NewEpoch {
					ends_on,
					reward: epoch_data.reward,
					last_changes,
				});
				Ok(())
			})
			.ok();
			T::WeightInfo::on_initialize(counter.groups, counter.weights, counter.currencies)
		}
	}
	impl<T: Config> Pallet<T> {
		pub fn apply_epoch_changes(
			counter: &mut ChangeCounter,
		) -> Result<(EpochData<T>, EpochChanges<T>), DispatchError> {
			NextEpochChanges::<T>::try_mutate(|changes| {
				ActiveEpochData::<T>::try_mutate(|epoch_data| {
					counter.groups = T::Rewards::distribute_reward_with_weights(
						epoch_data.reward,
						epoch_data.weights.iter().map(|(g, w)| (*g, *w)),
					)
					.map(|results| results.len() as u32)?;
					for (&group_id, &weight) in &changes.weights {
						epoch_data.weights.try_insert(group_id, weight).ok();
						counter.weights += 1;
					}
					for (currency_id, &group_id) in &changes.currencies.clone() {
						T::Rewards::attach_currency(currency_id.clone(), group_id)?;
						counter.currencies += 1;
					}
					epoch_data.reward = changes.reward.unwrap_or(epoch_data.reward);
					epoch_data.duration = changes.duration.unwrap_or(epoch_data.duration);
					Ok((epoch_data.clone(), mem::take(changes)))
				})
			})
		}
	}
	#[pallet::call]
	impl<T: Config> Pallet<T> {
		#[pallet::weight(T::WeightInfo::stake())]
		#[transactional]
		#[pallet::call_index(0)]
		pub fn stake(
			origin: OriginFor<T>,
			currency_id: T::CurrencyId,
			amount: T::Balance,
		) -> DispatchResult {
			let account_id = ensure_signed(origin)?;
			T::Rewards::deposit_stake(currency_id, &account_id, amount)
		}
		#[pallet::weight(T::WeightInfo::unstake())]
		#[transactional]
		#[pallet::call_index(1)]
		pub fn unstake(
			origin: OriginFor<T>,
			currency_id: T::CurrencyId,
			amount: T::Balance,
		) -> DispatchResult {
			let account_id = ensure_signed(origin)?;
			T::Rewards::withdraw_stake(currency_id, &account_id, amount)
		}
		#[pallet::weight(T::WeightInfo::claim_reward())]
		#[transactional]
		#[pallet::call_index(2)]
		pub fn claim_reward(origin: OriginFor<T>, currency_id: T::CurrencyId) -> DispatchResult {
			let account_id = ensure_signed(origin)?;
			T::Rewards::claim_reward(currency_id, &account_id).map(|_| ())
		}
		#[pallet::weight(T::WeightInfo::set_distributed_reward())]
		#[pallet::call_index(3)]
		pub fn set_distributed_reward(origin: OriginFor<T>, balance: T::Balance) -> DispatchResult {
			T::AdminOrigin::ensure_origin(origin)?;
			NextEpochChanges::<T>::mutate(|changes| changes.reward = Some(balance));
			Ok(())
		}
		#[pallet::weight(T::WeightInfo::set_epoch_duration())]
		#[pallet::call_index(4)]
		pub fn set_epoch_duration(origin: OriginFor<T>, duration: MomentOf<T>) -> DispatchResult {
			T::AdminOrigin::ensure_origin(origin)?;
			NextEpochChanges::<T>::mutate(|changes| changes.duration = Some(duration));
			Ok(())
		}
		#[pallet::weight(T::WeightInfo::set_group_weight())]
		#[pallet::call_index(5)]
		pub fn set_group_weight(
			origin: OriginFor<T>,
			group_id: T::GroupId,
			weight: T::Weight,
		) -> DispatchResult {
			T::AdminOrigin::ensure_origin(origin)?;
			NextEpochChanges::<T>::try_mutate(|changes| {
				changes
					.weights
					.try_insert(group_id, weight)
					.map_err(|_| Error::<T>::MaxChangesPerEpochReached)
			})?;
			Ok(())
		}
		#[pallet::weight(T::WeightInfo::set_currency_group())]
		#[pallet::call_index(6)]
		pub fn set_currency_group(
			origin: OriginFor<T>,
			currency_id: T::CurrencyId,
			group_id: T::GroupId,
		) -> DispatchResult {
			T::AdminOrigin::ensure_origin(origin)?;
			NextEpochChanges::<T>::try_mutate(|changes| {
				changes
					.currencies
					.try_insert(currency_id, group_id)
					.map_err(|_| Error::<T>::MaxChangesPerEpochReached)
			})?;
			Ok(())
		}
	}
}