// 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.

use super::{helper, InheritedCallWeightAttr};
use frame_support_procedural_tools::get_doc_literals;
use proc_macro2::Span;
use quote::ToTokens;
use std::collections::HashMap;
use syn::spanned::Spanned;

/// List of additional token to be used for parsing.
mod keyword {
	syn::custom_keyword!(Call);
	syn::custom_keyword!(OriginFor);
	syn::custom_keyword!(RuntimeOrigin);
	syn::custom_keyword!(weight);
	syn::custom_keyword!(call_index);
	syn::custom_keyword!(compact);
	syn::custom_keyword!(T);
	syn::custom_keyword!(pallet);
	syn::custom_keyword!(feeless_if);
	syn::custom_keyword!(authorize);
	syn::custom_keyword!(weight_of_authorize);
}

/// Definition of dispatchables typically `impl<T: Config> Pallet<T> { ... }`
pub struct CallDef {
	/// The where_clause used.
	pub where_clause: Option<syn::WhereClause>,
	/// A set of usage of instance, must be check for consistency with trait.
	pub instances: Vec<helper::InstanceUsage>,
	/// The index of call item in pallet module.
	pub index: usize,
	/// Information on methods (used for expansion).
	pub methods: Vec<CallVariantDef>,
	/// The span of the pallet::call attribute.
	pub attr_span: proc_macro2::Span,
	/// Docs, specified on the impl Block.
	pub docs: Vec<syn::Expr>,
}

/// The weight of a call or the weight of authorize.
#[derive(Clone)]
pub enum CallWeightDef {
	/// Explicitly set on the call itself with `#[pallet::weight(…)]` or
	/// `#[pallet::weight_of_authorize(…)]`. This value is used.
	Immediate(syn::Expr),

	/// The default value that should be set for dev-mode pallets. Usually zero.
	DevModeDefault,

	/// Inherits whatever value is configured on the pallet level.
	///
	/// The concrete value is not known at this point.
	Inherited(syn::Type),
}

impl CallWeightDef {
	fn try_from(
		weight: Option<syn::Expr>,
		inherited_call_weight: &Option<InheritedCallWeightAttr>,
		dev_mode: bool,
	) -> Option<Self> {
		match (weight, inherited_call_weight) {
			(Some(weight), _) => Some(CallWeightDef::Immediate(weight)),
			(None, Some(inherited)) => Some(CallWeightDef::Inherited(inherited.typename.clone())),
			(None, _) if dev_mode => Some(CallWeightDef::DevModeDefault),
			(None, _) => None,
		}
	}
}

/// Definition of dispatchable typically: `#[weight...] fn foo(origin .., param1: ...) -> ..`
#[derive(Clone)]
pub struct CallVariantDef {
	/// Function name.
	pub name: syn::Ident,
	/// Information on args: `(is_compact, name, type)`
	pub args: Vec<(bool, syn::Ident, Box<syn::Type>)>,
	/// Weight for the call.
	pub weight: CallWeightDef,
	/// Call index of the dispatchable.
	pub call_index: u8,
	/// Whether an explicit call index was specified.
	pub explicit_call_index: bool,
	/// Docs, used for metadata.
	pub docs: Vec<syn::Expr>,
	/// Attributes annotated at the top of the dispatchable function.
	pub attrs: Vec<syn::Attribute>,
	/// The `cfg` attributes.
	pub cfg_attrs: Vec<syn::Attribute>,
	/// The optional `feeless_if` attribute on the `pallet::call`.
	pub feeless_check: Option<syn::ExprClosure>,
	/// The return type of the call: `DispatchInfo` or `DispatchResultWithPostInfo`.
	pub return_type: helper::CallReturnType,
	/// The information related to `authorize` attribute.
	/// `(authorize expression, weight of authorize)`
	pub authorize: Option<AuthorizeDef>,
}

/// Definition related to the `authorize` attribute and other related attributes.
#[derive(Clone)]
pub struct AuthorizeDef {
	/// The expression of the authorize attribute.
	pub expr: syn::Expr,
	/// The weight of the authorize attribute as define by the attribute
	/// `[pallet::weight_of_authorize]`.
	pub weight: CallWeightDef,
}

/// Attributes for functions in call impl block.
pub enum FunctionAttr {
	/// Parse for `#[pallet::call_index(expr)]`
	CallIndex(u8),
	/// Parse for `#[pallet::weight(expr)]`
	Weight(syn::Expr),
	/// Parse for `#[pallet::feeless_if(expr)]`
	FeelessIf(Span, syn::ExprClosure),
	/// Parse for `#[pallet::authorize(expr)]`
	Authorize(syn::Expr),
	/// Parse for `#[pallet::weight_of_authorize(expr)]`
	WeightOfAuthorize(syn::Expr),
}

impl syn::parse::Parse for FunctionAttr {
	fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
		input.parse::<syn::Token![#]>()?;
		let content;
		syn::bracketed!(content in input);
		content.parse::<keyword::pallet>()?;
		content.parse::<syn::Token![::]>()?;

		let lookahead = content.lookahead1();
		if lookahead.peek(keyword::weight) {
			content.parse::<keyword::weight>()?;
			let weight_content;
			syn::parenthesized!(weight_content in content);
			Ok(FunctionAttr::Weight(weight_content.parse::<syn::Expr>()?))
		} else if lookahead.peek(keyword::call_index) {
			content.parse::<keyword::call_index>()?;
			let call_index_content;
			syn::parenthesized!(call_index_content in content);
			let index = call_index_content.parse::<syn::LitInt>()?;
			if !index.suffix().is_empty() {
				let msg = "Number literal must not have a suffix";
				return Err(syn::Error::new(index.span(), msg));
			}
			Ok(FunctionAttr::CallIndex(index.base10_parse()?))
		} else if lookahead.peek(keyword::feeless_if) {
			content.parse::<keyword::feeless_if>()?;
			let closure_content;
			syn::parenthesized!(closure_content in content);
			Ok(FunctionAttr::FeelessIf(
				closure_content.span(),
				closure_content.parse::<syn::ExprClosure>().map_err(|e| {
					let msg = "Invalid feeless_if attribute: expected a closure";
					let mut err = syn::Error::new(closure_content.span(), msg);
					err.combine(e);
					err
				})?,
			))
		} else if lookahead.peek(keyword::authorize) {
			content.parse::<keyword::authorize>()?;
			let closure_content;
			syn::parenthesized!(closure_content in content);
			Ok(FunctionAttr::Authorize(closure_content.parse::<syn::Expr>()?))
		} else if lookahead.peek(keyword::weight_of_authorize) {
			content.parse::<keyword::weight_of_authorize>()?;
			let closure_content;
			syn::parenthesized!(closure_content in content);
			Ok(FunctionAttr::WeightOfAuthorize(closure_content.parse::<syn::Expr>()?))
		} else {
			Err(lookahead.error())
		}
	}
}

/// Attribute for arguments in function in call impl block.
/// Parse for `#[pallet::compact]|
pub struct ArgAttrIsCompact;

impl syn::parse::Parse for ArgAttrIsCompact {
	fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
		input.parse::<syn::Token![#]>()?;
		let content;
		syn::bracketed!(content in input);
		content.parse::<keyword::pallet>()?;
		content.parse::<syn::Token![::]>()?;

		content.parse::<keyword::compact>()?;
		Ok(ArgAttrIsCompact)
	}
}

/// Check the syntax is `OriginFor<T>`, `&OriginFor<T>` or `T::RuntimeOrigin`.
pub fn check_dispatchable_first_arg_type(ty: &syn::Type, is_ref: bool) -> syn::Result<()> {
	pub struct CheckOriginFor(bool);
	impl syn::parse::Parse for CheckOriginFor {
		fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
			let is_ref = input.parse::<syn::Token![&]>().is_ok();
			input.parse::<keyword::OriginFor>()?;
			input.parse::<syn::Token![<]>()?;
			input.parse::<keyword::T>()?;
			input.parse::<syn::Token![>]>()?;

			Ok(Self(is_ref))
		}
	}

	pub struct CheckRuntimeOrigin;
	impl syn::parse::Parse for CheckRuntimeOrigin {
		fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
			input.parse::<keyword::T>()?;
			input.parse::<syn::Token![::]>()?;
			input.parse::<keyword::RuntimeOrigin>()?;

			Ok(Self)
		}
	}

	let result_origin_for = syn::parse2::<CheckOriginFor>(ty.to_token_stream());
	let result_runtime_origin = syn::parse2::<CheckRuntimeOrigin>(ty.to_token_stream());
	return match (result_origin_for, result_runtime_origin) {
		(Ok(CheckOriginFor(has_ref)), _) if is_ref == has_ref => Ok(()),
		(_, Ok(_)) => Ok(()),
		(_, _) => {
			let msg = if is_ref {
				"Invalid type: expected `&OriginFor<T>`"
			} else {
				"Invalid type: expected `OriginFor<T>` or `T::RuntimeOrigin`"
			};
			return Err(syn::Error::new(ty.span(), msg));
		},
	};
}

impl CallDef {
	pub fn try_from(
		attr_span: proc_macro2::Span,
		index: usize,
		item: &mut syn::Item,
		dev_mode: bool,
		inherited_call_weight: Option<InheritedCallWeightAttr>,
	) -> syn::Result<Self> {
		let item_impl = if let syn::Item::Impl(item) = item {
			item
		} else {
			return Err(syn::Error::new(item.span(), "Invalid pallet::call, expected item impl"));
		};

		crate::deprecation::prevent_deprecation_attr_on_outer_enum(&item_impl.attrs)?;

		let instances = vec![
			helper::check_impl_gen(&item_impl.generics, item_impl.impl_token.span())?,
			helper::check_pallet_struct_usage(&item_impl.self_ty)?,
		];

		if let Some((_, _, for_)) = item_impl.trait_ {
			let msg = "Invalid pallet::call, expected no trait ident as in \
				`impl<..> Pallet<..> { .. }`";
			return Err(syn::Error::new(for_.span(), msg));
		}

		let mut methods = vec![];
		let mut indices = HashMap::new();
		let mut last_index: Option<u8> = None;
		for item in &mut item_impl.items {
			if let syn::ImplItem::Fn(method) = item {
				if !matches!(method.vis, syn::Visibility::Public(_)) {
					let msg = "Invalid pallet::call, dispatchable function must be public: \
						`pub fn`";

					let span = match method.vis {
						syn::Visibility::Inherited => method.sig.span(),
						_ => method.vis.span(),
					};

					return Err(syn::Error::new(span, msg));
				}

				match method.sig.inputs.first() {
					None => {
						let msg = "Invalid pallet::call, must have at least origin arg";
						return Err(syn::Error::new(method.sig.span(), msg));
					},
					Some(syn::FnArg::Receiver(_)) => {
						let msg = "Invalid pallet::call, first argument must be a typed argument, \
							e.g. `origin: OriginFor<T>`";
						return Err(syn::Error::new(method.sig.span(), msg));
					},
					Some(syn::FnArg::Typed(arg)) => {
						check_dispatchable_first_arg_type(&arg.ty, false)?;
					},
				}

				let return_type = helper::check_pallet_call_return_type(&method.sig)?;

				let cfg_attrs: Vec<syn::Attribute> = helper::get_item_cfg_attrs(&method.attrs);
				let mut call_index = None;
				let mut weight = None;
				let mut feeless_check = None;
				let mut authorize = None;
				let mut weight_of_authorize = None;

				for attr in helper::take_item_pallet_attrs(&mut method.attrs)?.into_iter() {
					match attr {
						FunctionAttr::CallIndex(idx) => {
							if call_index.is_some() {
								let msg =
									"Invalid pallet::call, too many call_index attributes given";
								return Err(syn::Error::new(method.sig.span(), msg))
							}

							call_index = Some(idx);
						},
						FunctionAttr::Weight(w) => {
							if weight.is_some() {
								let msg = "Invalid pallet::call, too many weight attributes given";
								return Err(syn::Error::new(method.sig.span(), msg))
							}
							weight = Some(w);
						},
						FunctionAttr::FeelessIf(span, closure) => {
							if feeless_check.is_some() {
								let msg =
									"Invalid pallet::call, there can only be one feeless_if attribute";
								return Err(syn::Error::new(span, msg))
							}

							feeless_check = Some(closure);
						},
						FunctionAttr::Authorize(expr) => {
							if authorize.is_some() {
								let msg =
									"Invalid pallet::call, there can only be one authorize attribute";
								return Err(syn::Error::new(method.sig.span(), msg))
							}

							authorize = Some(expr);
						},
						FunctionAttr::WeightOfAuthorize(expr) => {
							if weight_of_authorize.is_some() {
								let msg = "Invalid pallet::call, there can only be one weight_of_authorize attribute";
								return Err(syn::Error::new(method.sig.span(), msg))
							}

							weight_of_authorize = Some(expr);
						},
					}
				}

				if weight_of_authorize.is_some() && authorize.is_none() {
					let msg = "Invalid pallet::call, weight_of_authorize attribute must be used with authorize attribute";
					return Err(syn::Error::new(weight_of_authorize.unwrap().span(), msg))
				}

				let authorize = if let Some(expr) = authorize {
					let weight_of_authorize = CallWeightDef::try_from(
						weight_of_authorize,
						&inherited_call_weight,
						dev_mode,
					)
					.ok_or_else(|| {
						syn::Error::new(
							method.sig.span(),
							"A pallet::call using authorize requires either a concrete \
							`#[pallet::weight_of_authorize($expr)]` or an inherited weight from \
							the `#[pallet:call(weight($type))]` attribute, but \
							none were given.",
						)
					})?;
					Some(AuthorizeDef { expr, weight: weight_of_authorize })
				} else {
					None
				};

				let weight = CallWeightDef::try_from(weight, &inherited_call_weight, dev_mode)
					.ok_or_else(|| {
						syn::Error::new(
							method.sig.span(),
							"A pallet::call requires either a concrete `#[pallet::weight($expr)]` \
							or an inherited weight from the `#[pallet:call(weight($type))]` \
							attribute, but none were given.",
						)
					})?;

				let explicit_call_index = call_index.is_some();

				let final_index = match call_index {
					Some(i) => i,
					None =>
						last_index.map_or(Some(0), |idx| idx.checked_add(1)).ok_or_else(|| {
							let msg = "Call index doesn't fit into u8, index is 256";
							syn::Error::new(method.sig.span(), msg)
						})?,
				};
				last_index = Some(final_index);

				if let Some(used_fn) = indices.insert(final_index, method.sig.ident.clone()) {
					let msg = format!(
						"Call indices are conflicting: Both functions {} and {} are at index {}",
						used_fn, method.sig.ident, final_index,
					);
					let mut err = syn::Error::new(used_fn.span(), &msg);
					err.combine(syn::Error::new(method.sig.ident.span(), msg));
					return Err(err);
				}

				let mut args = vec![];
				for arg in method.sig.inputs.iter_mut().skip(1) {
					let arg = if let syn::FnArg::Typed(arg) = arg {
						arg
					} else {
						unreachable!("Only first argument can be receiver");
					};

					let arg_attrs: Vec<ArgAttrIsCompact> =
						helper::take_item_pallet_attrs(&mut arg.attrs)?;

					if arg_attrs.len() > 1 {
						let msg = "Invalid pallet::call, argument has too many attributes";
						return Err(syn::Error::new(arg.span(), msg));
					}

					let arg_ident = if let syn::Pat::Ident(pat) = &*arg.pat {
						pat.ident.clone()
					} else {
						let msg = "Invalid pallet::call, argument must be ident";
						return Err(syn::Error::new(arg.pat.span(), msg));
					};

					args.push((!arg_attrs.is_empty(), arg_ident, arg.ty.clone()));
				}

				let docs = get_doc_literals(&method.attrs);

				if let Some(ref feeless_check) = feeless_check {
					if feeless_check.inputs.len() != args.len() + 1 {
						let msg = "Invalid pallet::call, feeless_if closure must have same \
							number of arguments as the dispatchable function";
						return Err(syn::Error::new(feeless_check.span(), msg));
					}

					match feeless_check.inputs.first() {
						None => {
							let msg = "Invalid pallet::call, feeless_if closure must have at least origin arg";
							return Err(syn::Error::new(feeless_check.span(), msg));
						},
						Some(syn::Pat::Type(arg)) => {
							check_dispatchable_first_arg_type(&arg.ty, true)?;
						},
						_ => {
							let msg = "Invalid pallet::call, feeless_if closure first argument must be a typed argument, \
								e.g. `origin: OriginFor<T>`";
							return Err(syn::Error::new(feeless_check.span(), msg));
						},
					}

					for (feeless_arg, arg) in feeless_check.inputs.iter().skip(1).zip(args.iter()) {
						let feeless_arg_type = if let syn::Pat::Type(syn::PatType { ty, .. }) =
							feeless_arg.clone()
						{
							if let syn::Type::Reference(pat) = *ty {
								pat.elem.clone()
							} else {
								let msg = "Invalid pallet::call, feeless_if closure argument must be a reference";
								return Err(syn::Error::new(ty.span(), msg));
							}
						} else {
							let msg = "Invalid pallet::call, feeless_if closure argument must be a type ascription pattern";
							return Err(syn::Error::new(feeless_arg.span(), msg));
						};

						if feeless_arg_type != arg.2 {
							let msg =
								"Invalid pallet::call, feeless_if closure argument must have \
								a reference to the same type as the dispatchable function argument";
							return Err(syn::Error::new(feeless_arg.span(), msg));
						}
					}

					let valid_return = match &feeless_check.output {
						syn::ReturnType::Type(_, type_) => match *(type_.clone()) {
							syn::Type::Path(syn::TypePath { path, .. }) => path.is_ident("bool"),
							_ => false,
						},
						_ => false,
					};
					if !valid_return {
						let msg = "Invalid pallet::call, feeless_if closure must return `bool`";
						return Err(syn::Error::new(feeless_check.output.span(), msg));
					}
				}

				methods.push(CallVariantDef {
					name: method.sig.ident.clone(),
					weight,
					call_index: final_index,
					explicit_call_index,
					args,
					docs,
					attrs: method.attrs.clone(),
					cfg_attrs,
					feeless_check,
					return_type,
					authorize,
				});
			} else {
				let msg = "Invalid pallet::call, only method accepted";
				return Err(syn::Error::new(item.span(), msg));
			}
		}

		Ok(Self {
			index,
			attr_span,
			instances,
			methods,
			where_clause: item_impl.generics.where_clause.clone(),
			docs: get_doc_literals(&item_impl.attrs),
		})
	}
}
