#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
pub(crate) mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
pub use pallet::*;
pub use weights::WeightInfo;
#[frame_support::pallet]
pub mod pallet {
	use cfg_primitives::conversion::convert_balance_decimals;
	use cfg_traits::{
		swaps::{OrderInfo, OrderRatio, Swap, SwapInfo, TokenSwaps},
		StatusNotificationHook, ValueProvider,
	};
	use frame_support::{
		pallet_prelude::{DispatchResult, Member, StorageDoubleMap, StorageValue, *},
		traits::{
			fungibles::{Inspect as AssetInspect, InspectHold, Mutate, MutateHold},
			tokens::{AssetId, Precision, Preservation},
		},
		Twox64Concat,
	};
	use frame_system::pallet_prelude::{OriginFor, *};
	use orml_traits::asset_registry::{self, Inspect as _};
	use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
	use scale_info::TypeInfo;
	use sp_arithmetic::traits::CheckedSub;
	use sp_runtime::{
		traits::{
			AtLeast32BitUnsigned, EnsureAdd, EnsureAddAssign, EnsureDiv, EnsureFixedPointNumber,
			EnsureMul, EnsureSub, EnsureSubAssign, MaybeSerializeDeserialize, One, Zero,
		},
		FixedPointNumber, FixedPointOperand, TokenError,
	};
	use sp_std::cmp::{min, Ordering};
	use super::*;
	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
	pub type BalanceOf<T> =
		<<T as Config>::Currency as AssetInspect<<T as frame_system::Config>::AccountId>>::Balance;
	#[pallet::pallet]
	#[pallet::storage_version(STORAGE_VERSION)]
	pub struct Pallet<T>(_);
	#[pallet::config]
	pub trait Config: frame_system::Config {
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
		type AssetRegistry: asset_registry::Inspect<
			AssetId = Self::CurrencyId,
			Balance = BalanceOf<Self>,
		>;
		type CurrencyId: AssetId
			+ Parameter
			+ Default
			+ Member
			+ Copy
			+ MaybeSerializeDeserialize
			+ Ord;
		type OrderIdNonce: Parameter
			+ Member
			+ AtLeast32BitUnsigned
			+ Default
			+ Copy
			+ EnsureAdd
			+ MaybeSerializeDeserialize
			+ MaxEncodedLen;
		type BalanceIn: Member
			+ Parameter
			+ FixedPointOperand
			+ AtLeast32BitUnsigned
			+ EnsureMul
			+ EnsureDiv
			+ MaxEncodedLen
			+ Into<BalanceOf<Self>>
			+ From<BalanceOf<Self>>;
		type BalanceOut: Member
			+ Parameter
			+ FixedPointOperand
			+ AtLeast32BitUnsigned
			+ EnsureMul
			+ EnsureDiv
			+ MaxEncodedLen
			+ Into<BalanceOf<Self>>
			+ From<BalanceOf<Self>>;
		type Currency: AssetInspect<Self::AccountId, AssetId = Self::CurrencyId>
			+ InspectHold<Self::AccountId, Reason = ()>
			+ MutateHold<Self::AccountId>
			+ Mutate<Self::AccountId>;
		type Ratio: Parameter
			+ Member
			+ FixedPointNumber
			+ EnsureMul
			+ EnsureDiv
			+ MaybeSerializeDeserialize
			+ MaxEncodedLen;
		#[pallet::constant]
		type MinFulfillmentAmountNative: Get<Self::BalanceOut>;
		#[pallet::constant]
		type NativeDecimals: Get<u32>;
		type FulfilledOrderHook: StatusNotificationHook<
			Id = Self::OrderIdNonce,
			Status = SwapInfo<Self::BalanceIn, Self::BalanceOut, Self::CurrencyId, Self::Ratio>,
			Error = DispatchError,
		>;
		type FeederId: Parameter + Member + Ord + MaxEncodedLen;
		type RatioProvider: ValueProvider<
			Self::FeederId,
			(Self::CurrencyId, Self::CurrencyId),
			Value = Self::Ratio,
		>;
		type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
		type Weights: WeightInfo;
	}
	#[derive(
		Clone, RuntimeDebugNoBound, Encode, Decode, Eq, PartialEq, MaxEncodedLen, TypeInfo,
	)]
	#[scale_info(skip_type_params(T))]
	pub struct Order<T: Config> {
		pub order_id: T::OrderIdNonce,
		pub placing_account: T::AccountId,
		pub currency_in: T::CurrencyId,
		pub currency_out: T::CurrencyId,
		pub amount_in: T::BalanceIn,
		pub amount_out: T::BalanceOut,
		pub amount_out_initial: T::BalanceOut,
		pub ratio: OrderRatio<T::Ratio>,
	}
	#[pallet::storage]
	pub type Orders<T: Config> = StorageMap<
		_,
		Twox64Concat,
		T::OrderIdNonce,
		Order<T>,
		ResultQuery<Error<T>::OrderNotFound>,
	>;
	#[pallet::storage]
	pub type UserOrders<T: Config> = StorageDoubleMap<
		_,
		Twox64Concat,
		T::AccountId,
		Twox64Concat,
		T::OrderIdNonce,
		(),
		ResultQuery<Error<T>::OrderNotFound>,
	>;
	#[pallet::storage]
	pub type OrderIdNonceStore<T: Config> = StorageValue<_, T::OrderIdNonce, ValueQuery>;
	#[pallet::storage]
	pub type MarketFeederId<T: Config> =
		StorageValue<_, T::FeederId, ResultQuery<Error<T>::MarketFeederNotFound>>;
	#[pallet::event]
	#[pallet::generate_deposit(pub(super) fn deposit_event)]
	pub enum Event<T: Config> {
		OrderCreated {
			order_id: T::OrderIdNonce,
			creator_account: T::AccountId,
			currency_in: T::CurrencyId,
			currency_out: T::CurrencyId,
			amount_out: T::BalanceOut,
			min_fulfillment_amount_out: T::BalanceOut,
			ratio: OrderRatio<T::Ratio>,
		},
		OrderCancelled {
			account: T::AccountId,
			order_id: T::OrderIdNonce,
		},
		OrderUpdated {
			order_id: T::OrderIdNonce,
			account: T::AccountId,
			amount_out: T::BalanceOut,
			ratio: OrderRatio<T::Ratio>,
			min_fulfillment_amount_out: T::BalanceOut,
		},
		OrderFulfillment {
			order_id: T::OrderIdNonce,
			placing_account: T::AccountId,
			fulfilling_account: T::AccountId,
			partial_fulfillment: bool,
			fulfillment_amount: T::BalanceOut,
			currency_in: T::CurrencyId,
			currency_out: T::CurrencyId,
			ratio: T::Ratio,
		},
		FeederChanged { feeder_id: T::FeederId },
	}
	#[pallet::error]
	#[derive(PartialEq)]
	pub enum Error<T> {
		SameCurrencyIds,
		BelowMinFulfillmentAmount,
		InvalidCurrencyId,
		OrderNotFound,
		Unauthorised,
		FulfillAmountTooLarge,
		MarketFeederNotFound,
		MarketRatioNotFound,
	}
	#[pallet::call]
	impl<T: Config> Pallet<T> {
		#[pallet::call_index(0)]
		#[pallet::weight(T::Weights::place_order())]
		pub fn place_order(
			origin: OriginFor<T>,
			currency_in: T::CurrencyId,
			currency_out: T::CurrencyId,
			amount_out: T::BalanceOut,
			ratio: OrderRatio<T::Ratio>,
		) -> DispatchResult {
			let account_id = ensure_signed(origin)?;
			Self::inner_place_order(
				account_id,
				currency_in,
				currency_out,
				amount_out,
				ratio,
				Self::min_fulfillment_amount(currency_out)?,
			)?;
			Ok(())
		}
		#[pallet::call_index(1)]
		#[pallet::weight(T::Weights::update_order())]
		pub fn update_order(
			origin: OriginFor<T>,
			order_id: T::OrderIdNonce,
			amount_out: T::BalanceOut,
			ratio: OrderRatio<T::Ratio>,
		) -> DispatchResult {
			let account_id = ensure_signed(origin)?;
			let order = Orders::<T>::get(order_id)?;
			ensure!(
				account_id == order.placing_account,
				Error::<T>::Unauthorised
			);
			Self::inner_update_order(
				order.clone(),
				amount_out,
				ratio,
				Self::min_fulfillment_amount(order.currency_out)?,
			)
		}
		#[pallet::call_index(2)]
		#[pallet::weight(T::Weights::cancel_order())]
		pub fn cancel_order(origin: OriginFor<T>, order_id: T::OrderIdNonce) -> DispatchResult {
			let account_id = ensure_signed(origin)?;
			let order = <Orders<T>>::get(order_id)?;
			ensure!(
				account_id == order.placing_account,
				Error::<T>::Unauthorised
			);
			<Self as TokenSwaps<T::AccountId>>::cancel_order(order_id)
		}
		#[pallet::call_index(3)]
		#[pallet::weight(T::Weights::fill_order())]
		pub fn fill_order(
			origin: OriginFor<T>,
			order_id: T::OrderIdNonce,
			amount_out: T::BalanceOut,
		) -> DispatchResult {
			let account_id = ensure_signed(origin)?;
			let order = <Orders<T>>::get(order_id)?;
			Self::fulfill_order_with_amount(order, amount_out, account_id)
		}
		#[pallet::call_index(4)]
		#[pallet::weight(T::Weights::set_market_feeder())]
		pub fn set_market_feeder(origin: OriginFor<T>, feeder_id: T::FeederId) -> DispatchResult {
			T::AdminOrigin::ensure_origin(origin)?;
			MarketFeederId::<T>::put(feeder_id.clone());
			Self::deposit_event(Event::<T>::FeederChanged { feeder_id });
			Ok(())
		}
	}
	impl<T: Config> Pallet<T> {
		fn inner_place_order(
			account: T::AccountId,
			currency_in: T::CurrencyId,
			currency_out: T::CurrencyId,
			amount_out: T::BalanceOut,
			ratio: OrderRatio<T::Ratio>,
			min_fulfillment_amount_out: T::BalanceOut,
		) -> Result<T::OrderIdNonce, DispatchError> {
			let order_id = OrderIdNonceStore::<T>::try_mutate(|n| {
				n.ensure_add_assign(One::one())?;
				Ok::<_, DispatchError>(*n)
			})?;
			ensure!(
				amount_out >= min_fulfillment_amount_out,
				Error::<T>::BelowMinFulfillmentAmount
			);
			ensure!(currency_in != currency_out, Error::<T>::SameCurrencyIds);
			T::Currency::hold(currency_out, &(), &account, amount_out.into())?;
			let new_order = Order {
				order_id,
				placing_account: account.clone(),
				currency_in,
				currency_out,
				amount_out,
				ratio,
				amount_out_initial: amount_out,
				amount_in: Zero::zero(),
			};
			Orders::<T>::insert(order_id, new_order.clone());
			UserOrders::<T>::insert(&account, order_id, ());
			Self::deposit_event(Event::OrderCreated {
				creator_account: account,
				ratio,
				order_id,
				amount_out,
				currency_in,
				currency_out,
				min_fulfillment_amount_out,
			});
			Ok(order_id)
		}
		fn inner_update_order(
			mut order: Order<T>,
			amount_out: T::BalanceOut,
			ratio: OrderRatio<T::Ratio>,
			min_fulfillment_amount_out: T::BalanceOut,
		) -> DispatchResult {
			ensure!(
				amount_out >= min_fulfillment_amount_out,
				Error::<T>::BelowMinFulfillmentAmount
			);
			match amount_out.cmp(&order.amount_out) {
				Ordering::Greater => {
					let amount_diff = amount_out.ensure_sub(order.amount_out)?;
					order.amount_out_initial.ensure_add_assign(amount_diff)?;
					T::Currency::hold(
						order.currency_out,
						&(),
						&order.placing_account,
						amount_diff.into(),
					)?;
				}
				Ordering::Less => {
					let amount_diff = order.amount_out.ensure_sub(amount_out)?;
					order.amount_out_initial.ensure_sub_assign(amount_diff)?;
					T::Currency::release(
						order.currency_out,
						&(),
						&order.placing_account,
						amount_diff.into(),
						Precision::Exact,
					)?;
				}
				Ordering::Equal => (),
			}
			order.amount_out = amount_out;
			order.ratio = ratio;
			Orders::<T>::insert(order.order_id, order.clone());
			Self::deposit_event(Event::OrderUpdated {
				account: order.placing_account,
				order_id: order.order_id,
				amount_out,
				ratio,
				min_fulfillment_amount_out,
			});
			Ok(())
		}
		pub fn remove_order(order_id: T::OrderIdNonce) -> DispatchResult {
			let order = <Orders<T>>::get(order_id)?;
			Orders::<T>::remove(order.order_id);
			UserOrders::<T>::remove(&order.placing_account, order.order_id);
			Ok(())
		}
		fn fulfill_order_with_amount(
			order: Order<T>,
			amount_out: T::BalanceOut,
			fulfilling_account: T::AccountId,
		) -> DispatchResult {
			let min_fulfillment_amount_out = min(
				order.amount_out,
				Self::min_fulfillment_amount(order.currency_out)?,
			);
			ensure!(
				amount_out >= min_fulfillment_amount_out,
				Error::<T>::BelowMinFulfillmentAmount,
			);
			let ratio = match order.ratio {
				OrderRatio::Market => Self::market_ratio(order.currency_out, order.currency_in)?,
				OrderRatio::Custom(ratio) => ratio,
			};
			let amount_in =
				Self::convert_with_ratio(order.currency_out, order.currency_in, ratio, amount_out)?;
			let remaining_amount_out = order
				.amount_out
				.checked_sub(&amount_out)
				.ok_or(Error::<T>::FulfillAmountTooLarge)?;
			let partial_fulfillment = !remaining_amount_out.is_zero();
			if partial_fulfillment {
				let mut updated_order = order.clone();
				updated_order.amount_out = remaining_amount_out;
				updated_order.amount_in = order.amount_in.ensure_add(amount_in)?;
				Orders::<T>::insert(updated_order.order_id, updated_order.clone());
			} else {
				Self::remove_order(order.order_id)?;
			}
			T::Currency::release(
				order.currency_out,
				&(),
				&order.placing_account,
				amount_out.into(),
				Precision::Exact,
			)?;
			if T::Currency::balance(order.currency_out, &order.placing_account) < amount_out.into()
			{
				Err(DispatchError::Token(TokenError::FundsUnavailable))?
			}
			if T::Currency::balance(order.currency_in, &fulfilling_account) < amount_in.into() {
				Err(DispatchError::Token(TokenError::FundsUnavailable))?
			}
			T::Currency::transfer(
				order.currency_out,
				&order.placing_account,
				&fulfilling_account,
				amount_out.into(),
				Preservation::Expendable,
			)?;
			T::Currency::transfer(
				order.currency_in,
				&fulfilling_account,
				&order.placing_account,
				amount_in.into(),
				Preservation::Expendable,
			)?;
			T::FulfilledOrderHook::notify_status_change(
				order.order_id,
				SwapInfo {
					remaining: Swap {
						amount_out: remaining_amount_out,
						currency_in: order.currency_in,
						currency_out: order.currency_out,
					},
					swapped_in: amount_in,
					swapped_out: amount_out,
					ratio,
				},
			)?;
			Self::deposit_event(Event::OrderFulfillment {
				order_id: order.order_id,
				placing_account: order.placing_account,
				fulfilling_account,
				partial_fulfillment,
				currency_in: order.currency_in,
				currency_out: order.currency_out,
				fulfillment_amount: amount_out,
				ratio,
			});
			Ok(())
		}
		pub fn market_ratio(
			currency_from: T::CurrencyId,
			currency_to: T::CurrencyId,
		) -> Result<T::Ratio, DispatchError> {
			let feeder = MarketFeederId::<T>::get()?;
			T::RatioProvider::get(&feeder, &(currency_from, currency_to))?
				.ok_or(Error::<T>::MarketRatioNotFound.into())
		}
		pub fn convert_with_ratio(
			currency_from: T::CurrencyId,
			currency_to: T::CurrencyId,
			ratio: T::Ratio,
			amount_from: T::BalanceOut,
		) -> Result<T::BalanceIn, DispatchError> {
			let from_decimals = T::AssetRegistry::metadata(¤cy_from)
				.ok_or(Error::<T>::InvalidCurrencyId)?
				.decimals;
			let to_decimals = T::AssetRegistry::metadata(¤cy_to)
				.ok_or(Error::<T>::InvalidCurrencyId)?
				.decimals;
			let amount_in = ratio.ensure_mul_int(amount_from)?;
			Ok(convert_balance_decimals(from_decimals, to_decimals, amount_in.into())?.into())
		}
		pub fn min_fulfillment_amount(
			currency: T::CurrencyId,
		) -> Result<T::BalanceOut, DispatchError> {
			let to_decimals = T::AssetRegistry::metadata(¤cy)
				.ok_or(Error::<T>::InvalidCurrencyId)?
				.decimals;
			Ok(convert_balance_decimals(
				T::NativeDecimals::get(),
				to_decimals,
				T::MinFulfillmentAmountNative::get().into(),
			)?
			.into())
		}
	}
	impl<T: Config> TokenSwaps<T::AccountId> for Pallet<T> {
		type BalanceIn = T::BalanceIn;
		type BalanceOut = T::BalanceOut;
		type CurrencyId = T::CurrencyId;
		type OrderId = T::OrderIdNonce;
		type Ratio = T::Ratio;
		fn place_order(
			account: T::AccountId,
			currency_in: T::CurrencyId,
			currency_out: T::CurrencyId,
			amount_out: T::BalanceOut,
			ratio: OrderRatio<T::Ratio>,
		) -> Result<Self::OrderId, DispatchError> {
			Self::inner_place_order(
				account,
				currency_in,
				currency_out,
				amount_out,
				ratio,
				T::BalanceOut::zero(),
			)
		}
		fn cancel_order(order: Self::OrderId) -> DispatchResult {
			let order = <Orders<T>>::get(order)?;
			let account_id = order.placing_account.clone();
			T::Currency::release(
				order.currency_out,
				&(),
				&order.placing_account,
				order.amount_out.into(),
				Precision::Exact,
			)?;
			Self::remove_order(order.order_id)?;
			Self::deposit_event(Event::OrderCancelled {
				account: account_id,
				order_id: order.order_id,
			});
			Ok(())
		}
		fn update_order(
			order_id: Self::OrderId,
			amount_out: T::BalanceOut,
			ratio: OrderRatio<T::Ratio>,
		) -> DispatchResult {
			let order = Orders::<T>::get(order_id)?;
			Self::inner_update_order(order, amount_out, ratio, T::BalanceOut::zero())
		}
		fn get_order_details(
			order: Self::OrderId,
		) -> Option<OrderInfo<Self::BalanceOut, Self::CurrencyId, Self::Ratio>> {
			Orders::<T>::get(order)
				.map(|order| OrderInfo {
					swap: Swap {
						currency_in: order.currency_in,
						currency_out: order.currency_out,
						amount_out: order.amount_out,
					},
					ratio: order.ratio,
				})
				.ok()
		}
		fn fill_order(
			account: T::AccountId,
			order_id: Self::OrderId,
			buy_amount: T::BalanceOut,
		) -> DispatchResult {
			let order = <Orders<T>>::get(order_id)?;
			Self::fulfill_order_with_amount(order, buy_amount, account)
		}
		fn convert_by_market(
			currency_in: Self::CurrencyId,
			currency_out: Self::CurrencyId,
			amount_out: T::BalanceOut,
		) -> Result<T::BalanceIn, DispatchError> {
			if currency_in == currency_out {
				let amount: BalanceOf<T> = amount_out.into();
				return Ok(amount.into());
			}
			let ratio = Self::market_ratio(currency_out, currency_in)?;
			Self::convert_with_ratio(currency_out, currency_in, ratio, amount_out)
		}
		fn market_ratio(
			currency_in: Self::CurrencyId,
			currency_out: Self::CurrencyId,
		) -> Result<Self::Ratio, DispatchError> {
			Self::market_ratio(currency_out, currency_in)
		}
	}
	#[cfg(feature = "runtime-benchmarks")]
	impl<T: Config> ValueProvider<(), (T::CurrencyId, T::CurrencyId)> for Pallet<T> {
		type Value = T::Ratio;
		fn get(
			_: &(),
			(currency_out, currency_in): &(T::CurrencyId, T::CurrencyId),
		) -> Result<Option<Self::Value>, DispatchError> {
			Self::market_ratio(*currency_out, *currency_in).map(Some)
		}
		fn set(_: &(), pair: &(T::CurrencyId, T::CurrencyId), value: Self::Value) {
			let feeder = MarketFeederId::<T>::get().unwrap();
			T::RatioProvider::set(&feeder, &pair, value);
		}
	}
}