// This file is part of Substrate.

// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 	http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Try-runtime specific traits and types.

pub mod decode_entire_state;
pub use decode_entire_state::{TryDecodeEntireStorage, TryDecodeEntireStorageError};

use super::StorageInstance;

use alloc::vec::Vec;
use impl_trait_for_tuples::impl_for_tuples;
use sp_arithmetic::traits::AtLeast32BitUnsigned;
use sp_runtime::TryRuntimeError;

/// Which state tests to execute.
#[derive(codec::Encode, codec::Decode, Clone, scale_info::TypeInfo, PartialEq)]
pub enum Select {
	/// None of them.
	None,
	/// All of them.
	All,
	/// Run a fixed number of them in a round robin manner.
	RoundRobin(u32),
	/// Run only pallets who's name matches the given list.
	///
	/// Pallet names are obtained from [`super::PalletInfoAccess`].
	Only(Vec<Vec<u8>>),
	/// Run all pallets except those whose names match the given list.
	///
	/// Pallet names are obtained from [`super::PalletInfoAccess`].
	AllExcept(Vec<Vec<u8>>),
}

impl Select {
	/// Whether to run any checks at all.
	pub fn any(&self) -> bool {
		!matches!(self, Select::None)
	}
}

impl Default for Select {
	fn default() -> Self {
		Select::None
	}
}

impl core::fmt::Debug for Select {
	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
		match self {
			Select::RoundRobin(x) => write!(f, "RoundRobin({})", x),
			Select::Only(x) => write!(
				f,
				"Only({:?})",
				x.iter()
					.map(|x| alloc::str::from_utf8(x).unwrap_or("<invalid?>"))
					.collect::<Vec<_>>(),
			),
			Select::AllExcept(x) => write!(
				f,
				"AllExcept({:?})",
				x.iter()
					.map(|x| alloc::str::from_utf8(x).unwrap_or("<invalid?>"))
					.collect::<Vec<_>>(),
			),
			Select::All => write!(f, "All"),
			Select::None => write!(f, "None"),
		}
	}
}

#[cfg(feature = "std")]
impl std::str::FromStr for Select {
	type Err = &'static str;
	fn from_str(s: &str) -> Result<Self, Self::Err> {
		match s {
			"all" | "All" => Ok(Select::All),
			"none" | "None" => Ok(Select::None),
			_ => {
				if s.starts_with("rr-") {
					let count = s
						.split_once('-')
						.and_then(|(_, count)| count.parse::<u32>().ok())
						.ok_or("failed to parse count")?;
					Ok(Select::RoundRobin(count))
				} else if s.starts_with("all-except-") {
					let pallets = s
						.strip_prefix("all-except-")
						.ok_or("failed to parse all-except prefix")?
						.split(',')
						.map(|x| x.as_bytes().to_vec())
						.collect::<Vec<_>>();
					Ok(Select::AllExcept(pallets))
				} else {
					let pallets = s.split(',').map(|x| x.as_bytes().to_vec()).collect::<Vec<_>>();
					Ok(Select::Only(pallets))
				}
			},
		}
	}
}

/// Select which checks should be run when trying a runtime upgrade upgrade.
#[derive(codec::Encode, codec::Decode, Clone, Debug, Copy, scale_info::TypeInfo, PartialEq)]
pub enum UpgradeCheckSelect {
	/// Run no checks.
	None,
	/// Run the `try_state`, `pre_upgrade` and `post_upgrade` checks.
	All,
	/// Run the `pre_upgrade` and `post_upgrade` checks.
	PreAndPost,
	/// Run the `try_state` checks.
	TryState,
}

impl UpgradeCheckSelect {
	/// Whether the pre- and post-upgrade checks are selected.
	pub fn pre_and_post(&self) -> bool {
		matches!(self, Self::All | Self::PreAndPost)
	}

	/// Whether the try-state checks are selected.
	pub fn try_state(&self) -> bool {
		matches!(self, Self::All | Self::TryState)
	}

	/// Whether to run any checks at all.
	pub fn any(&self) -> bool {
		!matches!(self, Self::None)
	}
}

#[cfg(feature = "std")]
impl core::str::FromStr for UpgradeCheckSelect {
	type Err = &'static str;

	fn from_str(s: &str) -> Result<Self, Self::Err> {
		match s.to_lowercase().as_str() {
			"none" => Ok(Self::None),
			"all" => Ok(Self::All),
			"pre-and-post" => Ok(Self::PreAndPost),
			"try-state" => Ok(Self::TryState),
			_ => Err("Invalid CheckSelector"),
		}
	}
}

/// Execute some checks to ensure the internal state of a pallet is consistent.
///
/// Usually, these checks should check all of the invariants that are expected to be held on all of
/// the storage items of your pallet.
///
/// This hook should not alter any storage.
pub trait TryState<BlockNumber> {
	/// Execute the state checks.
	fn try_state(_: BlockNumber, _: Select) -> Result<(), TryRuntimeError>;
}

#[cfg_attr(all(not(feature = "tuples-96"), not(feature = "tuples-128")), impl_for_tuples(64))]
#[cfg_attr(all(feature = "tuples-96", not(feature = "tuples-128")), impl_for_tuples(96))]
#[cfg_attr(all(feature = "tuples-128"), impl_for_tuples(128))]
impl<BlockNumber: Clone + core::fmt::Debug + AtLeast32BitUnsigned> TryState<BlockNumber> for Tuple {
	for_tuples!( where #( Tuple: crate::traits::PalletInfoAccess )* );
	fn try_state(n: BlockNumber, targets: Select) -> Result<(), TryRuntimeError> {
		match targets {
			Select::None => Ok(()),
			Select::All => {
				let mut errors = Vec::<TryRuntimeError>::new();

				for_tuples!(#(
					if let Err(err) = Tuple::try_state(n.clone(), targets.clone()) {
						errors.push(err);
					}
				)*);

				if !errors.is_empty() {
					log::error!(
						target: "try-runtime",
						"Detected errors while executing `try_state`:",
					);

					errors.iter().for_each(|err| {
						log::error!(
							target: "try-runtime",
							"{:?}",
							err
						);
					});

					return Err(
						"Detected errors while executing `try_state` checks. See logs for more \
						info."
							.into(),
					);
				}

				Ok(())
			},
			Select::RoundRobin(len) => {
				let functions: &[fn(BlockNumber, Select) -> Result<(), TryRuntimeError>] =
					&[for_tuples!(#( Tuple::try_state ),*)];
				let skip = n.clone() % (functions.len() as u32).into();
				let skip: u32 =
					skip.try_into().unwrap_or_else(|_| sp_runtime::traits::Bounded::max_value());
				let mut result = Ok(());
				for try_state_fn in functions.iter().cycle().skip(skip as usize).take(len as usize)
				{
					result = result.and(try_state_fn(n.clone(), targets.clone()));
				}
				result
			},
			Select::Only(ref pallet_names) => {
				let try_state_fns: &[(
					&'static str,
					fn(BlockNumber, Select) -> Result<(), TryRuntimeError>,
				)] = &[for_tuples!(
					#( (<Tuple as crate::traits::PalletInfoAccess>::name(), Tuple::try_state) ),*
				)];
				let mut result = Ok(());
				pallet_names.iter().for_each(|pallet_name| {
					if let Some((name, try_state_fn)) =
						try_state_fns.iter().find(|(name, _)| name.as_bytes() == pallet_name)
					{
						result = result.and(try_state_fn(n.clone(), targets.clone()));
					} else {
						log::warn!(
							"Pallet {:?} not found",
							alloc::str::from_utf8(pallet_name).unwrap_or_default()
						);
					}
				});

				result
			},
			Select::AllExcept(ref excluded_pallet_names) => {
				let try_state_fns: &[(
					&'static str,
					fn(BlockNumber, Select) -> Result<(), TryRuntimeError>,
				)] = &[for_tuples!(
					#( (<Tuple as crate::traits::PalletInfoAccess>::name(), Tuple::try_state) ),*
				)];

				excluded_pallet_names.iter().for_each(|excluded_name| {
					if !try_state_fns.iter().any(|(name, _)| name.as_bytes() == excluded_name) {
						log::warn!(
							"Pallet {:?} not found while trying to filter it out in Select::AllExcept",
							alloc::str::from_utf8(excluded_name).unwrap_or_default()
						);
					}
				});

				let try_state_fns: Vec<_> = try_state_fns
					.iter()
					.filter(|(name, _)| {
						!excluded_pallet_names
							.iter()
							.any(|excluded_name| name.as_bytes() == excluded_name)
					})
					.collect();

				let mut errors = Vec::<TryRuntimeError>::new();

				try_state_fns.iter().for_each(|(name, try_state_fn)| {
					if let Err(err) = try_state_fn(n.clone(), targets.clone()) {
						errors.push(err);
					}
				});

				if !errors.is_empty() {
					log::error!(
						target: "try-runtime",
						"Detected errors while executing `try_state`:",
					);

					errors.iter().for_each(|err| {
						log::error!(
							target: "try-runtime",
							"{:?}",
							err
						);
					});

					return Err(
						"Detected errors while executing `try_state` checks. See logs for more \
						info."
							.into(),
					);
				}

				Ok(())
			},
		}
	}
}
