use crate::{
	offchain::storage::{MutateStorageError, StorageRetrievalError, StorageValueRef},
	traits::BlockNumberProvider,
};
use codec::{Codec, Decode, Encode};
use core::fmt;
use sp_core::offchain::{Duration, Timestamp};
use sp_io::offchain;
const STORAGE_LOCK_DEFAULT_EXPIRY_DURATION: Duration = Duration::from_millis(20_000);
const STORAGE_LOCK_DEFAULT_EXPIRY_BLOCKS: u32 = 4;
const STORAGE_LOCK_PER_CHECK_ITERATION_SNOOZE_MIN: Duration = Duration::from_millis(10);
const STORAGE_LOCK_PER_CHECK_ITERATION_SNOOZE_MAX: Duration = Duration::from_millis(100);
pub trait Lockable: Sized {
	type Deadline: Sized + Codec + Clone;
	fn deadline(&self) -> Self::Deadline;
	fn has_expired(deadline: &Self::Deadline) -> bool;
	fn snooze(_deadline: &Self::Deadline) {
		sp_io::offchain::sleep_until(
			offchain::timestamp().add(STORAGE_LOCK_PER_CHECK_ITERATION_SNOOZE_MAX),
		);
	}
}
#[derive(Encode, Decode)]
pub struct Time {
	expiration_duration: Duration,
}
impl Default for Time {
	fn default() -> Self {
		Self { expiration_duration: STORAGE_LOCK_DEFAULT_EXPIRY_DURATION }
	}
}
impl Lockable for Time {
	type Deadline = Timestamp;
	fn deadline(&self) -> Self::Deadline {
		offchain::timestamp().add(self.expiration_duration)
	}
	fn has_expired(deadline: &Self::Deadline) -> bool {
		offchain::timestamp() > *deadline
	}
	fn snooze(deadline: &Self::Deadline) {
		let now = offchain::timestamp();
		let remainder: Duration = now.diff(deadline);
		let snooze = remainder.clamp(
			STORAGE_LOCK_PER_CHECK_ITERATION_SNOOZE_MIN,
			STORAGE_LOCK_PER_CHECK_ITERATION_SNOOZE_MAX,
		);
		sp_io::offchain::sleep_until(now.add(snooze));
	}
}
#[derive(Encode, Decode, Eq, PartialEq)]
pub struct BlockAndTimeDeadline<B: BlockNumberProvider> {
	pub block_number: <B as BlockNumberProvider>::BlockNumber,
	pub timestamp: Timestamp,
}
impl<B: BlockNumberProvider> Clone for BlockAndTimeDeadline<B> {
	fn clone(&self) -> Self {
		Self { block_number: self.block_number, timestamp: self.timestamp }
	}
}
impl<B: BlockNumberProvider> Default for BlockAndTimeDeadline<B> {
	fn default() -> Self {
		Self {
			block_number: B::current_block_number() + STORAGE_LOCK_DEFAULT_EXPIRY_BLOCKS.into(),
			timestamp: offchain::timestamp().add(STORAGE_LOCK_DEFAULT_EXPIRY_DURATION),
		}
	}
}
impl<B: BlockNumberProvider> fmt::Debug for BlockAndTimeDeadline<B>
where
	<B as BlockNumberProvider>::BlockNumber: fmt::Debug,
{
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		f.debug_struct("BlockAndTimeDeadline")
			.field("block_number", &self.block_number)
			.field("timestamp", &self.timestamp)
			.finish()
	}
}
pub struct BlockAndTime<B: BlockNumberProvider> {
	expiration_block_number_offset: u32,
	expiration_duration: Duration,
	_phantom: core::marker::PhantomData<B>,
}
impl<B: BlockNumberProvider> Default for BlockAndTime<B> {
	fn default() -> Self {
		Self {
			expiration_block_number_offset: STORAGE_LOCK_DEFAULT_EXPIRY_BLOCKS,
			expiration_duration: STORAGE_LOCK_DEFAULT_EXPIRY_DURATION,
			_phantom: core::marker::PhantomData::<B>,
		}
	}
}
impl<B: BlockNumberProvider> Clone for BlockAndTime<B> {
	fn clone(&self) -> Self {
		Self {
			expiration_block_number_offset: self.expiration_block_number_offset,
			expiration_duration: self.expiration_duration,
			_phantom: core::marker::PhantomData::<B>,
		}
	}
}
impl<B: BlockNumberProvider> Lockable for BlockAndTime<B> {
	type Deadline = BlockAndTimeDeadline<B>;
	fn deadline(&self) -> Self::Deadline {
		let block_number = <B as BlockNumberProvider>::current_block_number() +
			self.expiration_block_number_offset.into();
		BlockAndTimeDeadline {
			timestamp: offchain::timestamp().add(self.expiration_duration),
			block_number,
		}
	}
	fn has_expired(deadline: &Self::Deadline) -> bool {
		offchain::timestamp() > deadline.timestamp &&
			<B as BlockNumberProvider>::current_block_number() > deadline.block_number
	}
	fn snooze(deadline: &Self::Deadline) {
		let now = offchain::timestamp();
		let remainder: Duration = now.diff(&(deadline.timestamp));
		let snooze = remainder.clamp(
			STORAGE_LOCK_PER_CHECK_ITERATION_SNOOZE_MIN,
			STORAGE_LOCK_PER_CHECK_ITERATION_SNOOZE_MAX,
		);
		sp_io::offchain::sleep_until(now.add(snooze));
	}
}
pub struct StorageLock<'a, L = Time> {
	value_ref: StorageValueRef<'a>,
	lockable: L,
}
impl<'a, L: Lockable + Default> StorageLock<'a, L> {
	pub fn new(key: &'a [u8]) -> Self {
		Self::with_lockable(key, Default::default())
	}
}
impl<'a, L: Lockable> StorageLock<'a, L> {
	pub fn with_lockable(key: &'a [u8], lockable: L) -> Self {
		Self { value_ref: StorageValueRef::<'a>::persistent(key), lockable }
	}
	fn extend_active_lock(&mut self) -> Result<<L as Lockable>::Deadline, ()> {
		let res = self.value_ref.mutate(
			|s: Result<Option<L::Deadline>, StorageRetrievalError>| -> Result<<L as Lockable>::Deadline, ()> {
			match s {
				Ok(Some(deadline)) if !<L as Lockable>::has_expired(&deadline) =>
					Ok(self.lockable.deadline()),
				_ => Err(()),
			}
		});
		match res {
			Ok(deadline) => Ok(deadline),
			Err(MutateStorageError::ConcurrentModification(_)) => Err(()),
			Err(MutateStorageError::ValueFunctionFailed(e)) => Err(e),
		}
	}
	fn try_lock_inner(
		&mut self,
		new_deadline: L::Deadline,
	) -> Result<(), <L as Lockable>::Deadline> {
		let res = self.value_ref.mutate(
			|s: Result<Option<L::Deadline>, StorageRetrievalError>|
			-> Result<<L as Lockable>::Deadline, <L as Lockable>::Deadline> {
				match s {
					Ok(None) => Ok(new_deadline),
					Err(_) => Ok(new_deadline),
					Ok(Some(deadline)) if <L as Lockable>::has_expired(&deadline) =>
						Ok(new_deadline),
					Ok(Some(deadline)) => Err(deadline),
				}
			},
		);
		match res {
			Ok(_) => Ok(()),
			Err(MutateStorageError::ConcurrentModification(deadline)) => Err(deadline),
			Err(MutateStorageError::ValueFunctionFailed(e)) => Err(e),
		}
	}
	pub fn try_lock(&mut self) -> Result<StorageLockGuard<'a, '_, L>, <L as Lockable>::Deadline> {
		self.try_lock_inner(self.lockable.deadline())?;
		Ok(StorageLockGuard::<'a, '_> { lock: Some(self) })
	}
	pub fn lock(&mut self) -> StorageLockGuard<'a, '_, L> {
		while let Err(deadline) = self.try_lock_inner(self.lockable.deadline()) {
			L::snooze(&deadline);
		}
		StorageLockGuard::<'a, '_, L> { lock: Some(self) }
	}
	fn unlock(&mut self) {
		self.value_ref.clear();
	}
}
pub struct StorageLockGuard<'a, 'b, L: Lockable> {
	lock: Option<&'b mut StorageLock<'a, L>>,
}
impl<'a, 'b, L: Lockable> StorageLockGuard<'a, 'b, L> {
	pub fn forget(mut self) {
		let _ = self.lock.take();
	}
	pub fn extend_lock(&mut self) -> Result<<L as Lockable>::Deadline, ()> {
		if let Some(ref mut lock) = self.lock {
			lock.extend_active_lock()
		} else {
			Err(())
		}
	}
}
impl<'a, 'b, L: Lockable> Drop for StorageLockGuard<'a, 'b, L> {
	fn drop(&mut self) {
		if let Some(lock) = self.lock.take() {
			lock.unlock();
		}
	}
}
impl<'a> StorageLock<'a, Time> {
	pub fn with_deadline(key: &'a [u8], expiration_duration: Duration) -> Self {
		Self {
			value_ref: StorageValueRef::<'a>::persistent(key),
			lockable: Time { expiration_duration },
		}
	}
}
impl<'a, B> StorageLock<'a, BlockAndTime<B>>
where
	B: BlockNumberProvider,
{
	pub fn with_block_and_time_deadline(
		key: &'a [u8],
		expiration_block_number_offset: u32,
		expiration_duration: Duration,
	) -> Self {
		Self {
			value_ref: StorageValueRef::<'a>::persistent(key),
			lockable: BlockAndTime::<B> {
				expiration_block_number_offset,
				expiration_duration,
				_phantom: core::marker::PhantomData,
			},
		}
	}
	pub fn with_block_deadline(key: &'a [u8], expiration_block_number_offset: u32) -> Self {
		Self {
			value_ref: StorageValueRef::<'a>::persistent(key),
			lockable: BlockAndTime::<B> {
				expiration_block_number_offset,
				expiration_duration: STORAGE_LOCK_DEFAULT_EXPIRY_DURATION,
				_phantom: core::marker::PhantomData,
			},
		}
	}
}
#[cfg(test)]
mod tests {
	use super::*;
	use sp_core::offchain::{testing, OffchainDbExt, OffchainWorkerExt};
	use sp_io::TestExternalities;
	const VAL_1: u32 = 0u32;
	const VAL_2: u32 = 0xFFFF_FFFFu32;
	#[test]
	fn storage_lock_write_unlock_lock_read_unlock() {
		let (offchain, state) = testing::TestOffchainExt::new();
		let mut t = TestExternalities::default();
		t.register_extension(OffchainDbExt::new(offchain.clone()));
		t.register_extension(OffchainWorkerExt::new(offchain));
		t.execute_with(|| {
			let mut lock = StorageLock::<'_, Time>::new(b"lock_1");
			let val = StorageValueRef::persistent(b"protected_value");
			{
				let _guard = lock.lock();
				val.set(&VAL_1);
				assert_eq!(val.get::<u32>(), Ok(Some(VAL_1)));
			}
			{
				let _guard = lock.lock();
				val.set(&VAL_2);
				assert_eq!(val.get::<u32>(), Ok(Some(VAL_2)));
			}
		});
		assert_eq!(state.read().persistent_storage.get(b"lock_1"), None);
	}
	#[test]
	fn storage_lock_and_forget() {
		let (offchain, state) = testing::TestOffchainExt::new();
		let mut t = TestExternalities::default();
		t.register_extension(OffchainDbExt::new(offchain.clone()));
		t.register_extension(OffchainWorkerExt::new(offchain));
		t.execute_with(|| {
			let mut lock = StorageLock::<'_, Time>::new(b"lock_2");
			let val = StorageValueRef::persistent(b"protected_value");
			let guard = lock.lock();
			val.set(&VAL_1);
			assert_eq!(val.get::<u32>(), Ok(Some(VAL_1)));
			guard.forget();
		});
		let opt = state.read().persistent_storage.get(b"lock_2");
		assert!(opt.is_some());
	}
	#[test]
	fn storage_lock_and_let_expire_and_lock_again() {
		let (offchain, state) = testing::TestOffchainExt::new();
		let mut t = TestExternalities::default();
		t.register_extension(OffchainDbExt::new(offchain.clone()));
		t.register_extension(OffchainWorkerExt::new(offchain));
		t.execute_with(|| {
			let sleep_until = offchain::timestamp().add(Duration::from_millis(500));
			let lock_expiration = Duration::from_millis(200);
			let mut lock = StorageLock::<'_, Time>::with_deadline(b"lock_3", lock_expiration);
			{
				let guard = lock.lock();
				guard.forget();
			}
			offchain::sleep_until(sleep_until);
			let mut lock = StorageLock::<'_, Time>::new(b"lock_3");
			let res = lock.try_lock();
			assert!(res.is_ok());
			let guard = res.unwrap();
			guard.forget();
		});
		let opt = state.read().persistent_storage.get(b"lock_3");
		assert!(opt.is_some());
	}
	#[test]
	fn extend_active_lock() {
		let (offchain, state) = testing::TestOffchainExt::new();
		let mut t = TestExternalities::default();
		t.register_extension(OffchainDbExt::new(offchain.clone()));
		t.register_extension(OffchainWorkerExt::new(offchain));
		t.execute_with(|| {
			let lock_expiration = Duration::from_millis(300);
			let mut lock = StorageLock::<'_, Time>::with_deadline(b"lock_4", lock_expiration);
			let mut guard = lock.lock();
			offchain::sleep_until(offchain::timestamp().add(Duration::from_millis(200)));
			assert_eq!(guard.extend_lock().is_ok(), true);
			offchain::sleep_until(offchain::timestamp().add(Duration::from_millis(200)));
			let mut lock = StorageLock::<'_, Time>::with_deadline(b"lock_4", lock_expiration);
			let res = lock.try_lock();
			assert_eq!(res.is_ok(), false);
			offchain::sleep_until(offchain::timestamp().add(Duration::from_millis(200)));
			assert_eq!(guard.extend_lock().is_ok(), false);
			guard.forget();
			let mut lock = StorageLock::<'_, Time>::with_deadline(b"lock_4", lock_expiration);
			let res = lock.try_lock();
			assert!(res.is_ok());
			let guard = res.unwrap();
			guard.forget();
		});
		let opt = state.read().persistent_storage.get(b"lock_4");
		assert_eq!(opt.unwrap(), vec![132_u8, 3u8, 0, 0, 0, 0, 0, 0]); }
}