// This file is part of Substrate.

// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

use schnellru::{Limiter, LruMap};
use sp_runtime::{traits::Block as BlockT, Justifications};

const LOG_TARGET: &str = "db::pin";
const PINNING_CACHE_SIZE: usize = 2048;

/// Entry for pinned blocks cache.
struct PinnedBlockCacheEntry<Block: BlockT> {
	/// How many times this item has been pinned
	ref_count: u32,

	/// Cached justifications for this block
	pub justifications: Option<Option<Justifications>>,

	/// Cached body for this block
	pub body: Option<Option<Vec<Block::Extrinsic>>>,
}

impl<Block: BlockT> Default for PinnedBlockCacheEntry<Block> {
	fn default() -> Self {
		Self { ref_count: 0, justifications: None, body: None }
	}
}

impl<Block: BlockT> PinnedBlockCacheEntry<Block> {
	pub fn decrease_ref(&mut self) {
		self.ref_count = self.ref_count.saturating_sub(1);
	}

	pub fn increase_ref(&mut self) {
		self.ref_count = self.ref_count.saturating_add(1);
	}

	pub fn has_no_references(&self) -> bool {
		self.ref_count == 0
	}
}

/// A limiter for a map which is limited by the number of elements.
#[derive(Copy, Clone, Debug)]
struct LoggingByLengthLimiter {
	max_length: usize,
}

impl LoggingByLengthLimiter {
	/// Creates a new length limiter with a given `max_length`.
	pub const fn new(max_length: usize) -> LoggingByLengthLimiter {
		LoggingByLengthLimiter { max_length }
	}
}

impl<Block: BlockT> Limiter<Block::Hash, PinnedBlockCacheEntry<Block>> for LoggingByLengthLimiter {
	type KeyToInsert<'a> = Block::Hash;
	type LinkType = usize;

	fn is_over_the_limit(&self, length: usize) -> bool {
		length > self.max_length
	}

	fn on_insert(
		&mut self,
		_length: usize,
		key: Self::KeyToInsert<'_>,
		value: PinnedBlockCacheEntry<Block>,
	) -> Option<(Block::Hash, PinnedBlockCacheEntry<Block>)> {
		if self.max_length > 0 {
			Some((key, value))
		} else {
			None
		}
	}

	fn on_replace(
		&mut self,
		_length: usize,
		_old_key: &mut Block::Hash,
		_new_key: Block::Hash,
		_old_value: &mut PinnedBlockCacheEntry<Block>,
		_new_value: &mut PinnedBlockCacheEntry<Block>,
	) -> bool {
		true
	}

	fn on_removed(&mut self, key: &mut Block::Hash, value: &mut PinnedBlockCacheEntry<Block>) {
		// If reference count was larger than 0 on removal,
		// the item was removed due to capacity limitations.
		// Since the cache should be large enough for pinned items,
		// we want to know about these evictions.
		if value.ref_count > 0 {
			log::warn!(
				target: LOG_TARGET,
				"Pinned block cache limit reached. Evicting value. hash = {}",
				key
			);
		} else {
			log::trace!(
				target: LOG_TARGET,
				"Evicting value from pinned block cache. hash = {}",
				key
			)
		}
	}

	fn on_cleared(&mut self) {}

	fn on_grow(&mut self, _new_memory_usage: usize) -> bool {
		true
	}
}

/// Reference counted cache for pinned block bodies and justifications.
pub struct PinnedBlocksCache<Block: BlockT> {
	cache: LruMap<Block::Hash, PinnedBlockCacheEntry<Block>, LoggingByLengthLimiter>,
}

impl<Block: BlockT> PinnedBlocksCache<Block> {
	pub fn new() -> Self {
		Self { cache: LruMap::new(LoggingByLengthLimiter::new(PINNING_CACHE_SIZE)) }
	}

	/// Increase reference count of an item.
	/// Create an entry with empty value in the cache if necessary.
	pub fn pin(&mut self, hash: Block::Hash) {
		match self.cache.get_or_insert(hash, Default::default) {
			Some(entry) => {
				entry.increase_ref();
				log::trace!(
					target: LOG_TARGET,
					"Bumped cache refcount. hash = {}, num_entries = {}",
					hash,
					self.cache.len()
				);
			},
			None => {
				log::warn!(target: LOG_TARGET, "Unable to bump reference count. hash = {}", hash)
			},
		};
	}

	/// Clear the cache
	pub fn clear(&mut self) {
		self.cache.clear();
	}

	/// Check if item is contained in the cache
	pub fn contains(&self, hash: Block::Hash) -> bool {
		self.cache.peek(&hash).is_some()
	}

	/// Attach body to an existing cache item
	pub fn insert_body(&mut self, hash: Block::Hash, extrinsics: Option<Vec<Block::Extrinsic>>) {
		match self.cache.peek_mut(&hash) {
			Some(entry) => {
				entry.body = Some(extrinsics);
				log::trace!(
					target: LOG_TARGET,
					"Cached body. hash = {}, num_entries = {}",
					hash,
					self.cache.len()
				);
			},
			None => log::warn!(
				target: LOG_TARGET,
				"Unable to insert body for uncached item. hash = {}",
				hash
			),
		}
	}

	/// Attach justification to an existing cache item
	pub fn insert_justifications(
		&mut self,
		hash: Block::Hash,
		justifications: Option<Justifications>,
	) {
		match self.cache.peek_mut(&hash) {
			Some(entry) => {
				entry.justifications = Some(justifications);
				log::trace!(
					target: LOG_TARGET,
					"Cached justification. hash = {}, num_entries = {}",
					hash,
					self.cache.len()
				);
			},
			None => log::warn!(
				target: LOG_TARGET,
				"Unable to insert justifications for uncached item. hash = {}",
				hash
			),
		}
	}

	/// Decreases reference count of an item.
	/// If the count hits 0, the item is removed.
	pub fn unpin(&mut self, hash: Block::Hash) {
		if let Some(entry) = self.cache.peek_mut(&hash) {
			entry.decrease_ref();
			if entry.has_no_references() {
				self.cache.remove(&hash);
			}
		}
	}

	/// Get justifications for cached block
	pub fn justifications(&self, hash: &Block::Hash) -> Option<&Option<Justifications>> {
		self.cache.peek(hash).and_then(|entry| entry.justifications.as_ref())
	}

	/// Get body for cached block
	pub fn body(&self, hash: &Block::Hash) -> Option<&Option<Vec<Block::Extrinsic>>> {
		self.cache.peek(hash).and_then(|entry| entry.body.as_ref())
	}
}
