//! Individual implementation for all traits.

pub mod clone;
mod common_ord;
pub mod copy;
pub mod debug;
pub mod default;
#[cfg(feature = "serde")]
pub mod deserialize;
pub mod eq;
pub mod hash;
pub mod ord;
pub mod partial_eq;
pub mod partial_ord;
#[cfg(feature = "serde")]
mod serde;
#[cfg(feature = "serde")]
pub mod serialize;
#[cfg(feature = "zeroize")]
pub mod zeroize;
#[cfg(feature = "zeroize")]
pub mod zeroize_on_drop;

use std::{borrow::Cow, ops::Deref};

use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{
	parse::{Parse, ParseStream},
	punctuated::Punctuated,
	spanned::Spanned,
	Attribute, DeriveInput, Ident, ImplGenerics, Meta, Path, Result, Token, TraitBound,
	TraitBoundModifier, TypeGenerics, TypeParamBound, WhereClause,
};

use crate::{util::MetaListExt, Data, DeriveWhere, Error, Item, SplitGenerics};

/// Type implementing [`TraitImpl`] for every trait.
#[derive(Clone, Copy, Eq, PartialEq)]
#[cfg_attr(test, derive(Debug))]
pub enum Trait {
	/// [`Clone`].
	Clone,
	/// [`Copy`].
	Copy,
	/// [`Debug`](std::fmt::Debug).
	Debug,
	/// [`Default`].
	Default,
	/// [`Deserialize`](https://docs.rs/serde/latest/serde/derive.Deserialize.html).
	#[cfg(feature = "serde")]
	Deserialize,
	/// [`Eq`].
	Eq,
	/// [`Hash`](std::hash::Hash).
	Hash,
	/// [`Ord`].
	Ord,
	/// [`PartialEq`].
	PartialEq,
	/// [`PartialOrd`].
	PartialOrd,
	/// [`Serialize`](https://docs.rs/serde/latest/serde/derive.Serialize.html).
	#[cfg(feature = "serde")]
	Serialize,
	/// [`Zeroize`](https://docs.rs/zeroize/latest/zeroize/trait.Zeroize.html).
	#[cfg(feature = "zeroize")]
	Zeroize,
	/// [`ZeroizeOnDrop`](https://docs.rs/zeroize/latest/zeroize/trait.ZeroizeOnDrop.html).
	#[cfg(feature = "zeroize")]
	ZeroizeOnDrop,
}

/// Call a function on the corresponding trait struct matching the trait enum
/// variant.
macro_rules! trait_dispatch {
	($self:expr, $method:ident($($par:expr),*)) => {
		match $self {
			Trait::Clone => clone::Clone::$method($($par),*),
			Trait::Copy => copy::Copy::$method($($par),*),
			Trait::Debug => debug::Debug::$method($($par),*),
			Trait::Default => default::Default::$method($($par),*),
			#[cfg(feature = "serde")]
			Trait::Deserialize => deserialize::Deserialize::$method($($par),*),
			Trait::Eq => eq::Eq::$method($($par),*),
			Trait::Hash => hash::Hash::$method($($par),*),
			Trait::Ord => ord::Ord::$method($($par),*),
			Trait::PartialEq => partial_eq::PartialEq::$method($($par),*),
			Trait::PartialOrd => partial_ord::PartialOrd::$method($($par),*),
			#[cfg(feature = "serde")]
			Trait::Serialize => serialize::Serialize::$method($($par),*),
			#[cfg(feature = "zeroize")]
			Trait::Zeroize => zeroize::Zeroize::$method($($par),*),
			#[cfg(feature = "zeroize")]
			Trait::ZeroizeOnDrop => zeroize_on_drop::ZeroizeOnDrop::$method($($par),*),
		}
	};
}

impl Trait {
	/// Create [`Trait`] from [`Path`].
	pub fn from_path(path: &Path) -> Result<Self> {
		if let Some(ident) = path.get_ident() {
			use Trait::*;

			match ident.to_string().as_str() {
				"Clone" => Ok(Clone),
				"Copy" => Ok(Copy),
				"Debug" => Ok(Debug),
				"Default" => Ok(Default),
				#[cfg(feature = "serde")]
				"Deserialize" => Ok(Deserialize),
				#[cfg(not(feature = "serde"))]
				"Deserialize" => Err(Error::serde_feature(path.span())),
				"Eq" => Ok(Eq),
				"Hash" => Ok(Hash),
				"Ord" => Ok(Ord),
				"PartialEq" => Ok(PartialEq),
				"PartialOrd" => Ok(PartialOrd),
				#[cfg(feature = "serde")]
				"Serialize" => Ok(Serialize),
				#[cfg(not(feature = "serde"))]
				"Serialize" => Err(Error::serde_feature(path.span())),
				#[cfg(feature = "zeroize")]
				"Zeroize" => Ok(Zeroize),
				#[cfg(not(feature = "zeroize"))]
				"Zeroize" => Err(Error::zeroize_feature(path.span())),
				#[cfg(feature = "zeroize")]
				"ZeroizeOnDrop" => Ok(ZeroizeOnDrop),
				#[cfg(not(feature = "zeroize"))]
				"ZeroizeOnDrop" => Err(Error::zeroize_feature(path.span())),
				"crate" => Err(Error::crate_(path.span())),
				_ => Err(Error::trait_(path.span())),
			}
		} else {
			Err(Error::trait_(path.span()))
		}
	}

	/// Re-direct to [`TraitImpl::as_str()`].
	pub fn as_str(&self) -> &'static str {
		trait_dispatch!(self, as_str())
	}

	/// Re-direct to [`TraitImpl::default_derive_trait()`].
	pub fn default_derive_trait(&self) -> DeriveTrait {
		trait_dispatch!(self, default_derive_trait())
	}

	/// Re-direct to [`TraitImpl::parse_derive_trait()`].
	pub fn parse_derive_trait(
		&self,
		attrs: &[Attribute],
		span: Span,
		list: Option<Punctuated<Meta, Token![,]>>,
	) -> Result<DeriveTrait> {
		trait_dispatch!(self, parse_derive_trait(attrs, span, list))
	}

	/// Re-direct to [`TraitImpl::supports_union()`].
	pub fn supports_union(&self) -> bool {
		trait_dispatch!(self, supports_union())
	}

	/// Re-direct to [`TraitImpl::additional_where_bounds()`].
	pub fn additional_where_bounds(&self, data: &Item) -> Option<TypeParamBound> {
		trait_dispatch!(self, additional_where_bounds(data))
	}
}

/// Trait to implement.
#[derive(Eq, PartialEq)]
pub enum DeriveTrait {
	/// [`Clone`].
	Clone,
	/// [`Copy`].
	Copy,
	/// [`Debug`](std::fmt::Debug).
	Debug,
	/// [`Default`].
	Default,
	/// [`Deserialize`](https://docs.rs/serde/latest/serde/derive.Deserialize.html).
	#[cfg(feature = "serde")]
	Deserialize(deserialize::Deserialize),
	/// [`Eq`].
	Eq,
	/// [`Hash`](std::hash::Hash).
	Hash,
	/// [`Ord`].
	Ord,
	/// [`PartialEq`].
	PartialEq,
	/// [`PartialOrd`].
	PartialOrd,
	/// [`Serialize`](https://docs.rs/serde/latest/serde/derive.Serialize.html).
	#[cfg(feature = "serde")]
	Serialize(serialize::Serialize),
	/// [`Zeroize`](https://docs.rs/zeroize/latest/zeroize/trait.Zeroize.html).
	#[cfg(feature = "zeroize")]
	Zeroize(zeroize::Zeroize),
	/// [`ZeroizeOnDrop`](https://docs.rs/zeroize/latest/zeroize/trait.ZeroizeOnDrop.html).
	#[cfg(feature = "zeroize")]
	ZeroizeOnDrop(zeroize_on_drop::ZeroizeOnDrop),
}

impl Deref for DeriveTrait {
	type Target = dyn TraitImpl;

	fn deref(&self) -> &Self::Target {
		use DeriveTrait::*;

		match self {
			Clone => &clone::Clone,
			Copy => &copy::Copy,
			Debug => &debug::Debug,
			Default => &default::Default,
			#[cfg(feature = "serde")]
			Deserialize(trait_) => trait_,
			Eq => &eq::Eq,
			Hash => &hash::Hash,
			Ord => &ord::Ord,
			PartialEq => &partial_eq::PartialEq,
			PartialOrd => &partial_ord::PartialOrd,
			#[cfg(feature = "serde")]
			Serialize(trait_) => trait_,
			#[cfg(feature = "zeroize")]
			Zeroize(trait_) => trait_,
			#[cfg(feature = "zeroize")]
			ZeroizeOnDrop(trait_) => trait_,
		}
	}
}

impl PartialEq<Trait> for &DeriveTrait {
	fn eq(&self, other: &Trait) -> bool {
		let trait_: &Trait = self;
		trait_ == other
	}
}

impl DeriveTrait {
	/// Returns where-clause bounds for the trait in respect of the item type.
	pub fn where_bounds(&self, data: &Item) -> Punctuated<TypeParamBound, Token![+]> {
		let mut list = Punctuated::new();

		list.push(TypeParamBound::Trait(TraitBound {
			paren_token: None,
			modifier: TraitBoundModifier::None,
			lifetimes: None,
			path: self.path(),
		}));

		// Add bounds specific to the trait.
		if let Some(bound) = self.additional_where_bounds(data) {
			list.push(bound)
		}

		list
	}

	/// Create [`DeriveTrait`] from [`ParseStream`].
	pub fn from_stream(
		attrs: &[Attribute],
		span: Span,
		data: &syn::Data,
		input: ParseStream,
	) -> Result<(Span, Self)> {
		match Meta::parse(input) {
			Ok(meta) => {
				let trait_ = Trait::from_path(meta.path())?;

				if let syn::Data::Union(_) = data {
					// Make sure this `Trait` supports unions.
					if !trait_.supports_union() {
						return Err(Error::union(span));
					}
				}

				match &meta {
					Meta::Path(path) => Ok((
						path.span(),
						trait_.parse_derive_trait(attrs, meta.span(), None)?,
					)),
					Meta::List(list) => {
						let nested = list.parse_non_empty_nested_metas()?;

						// This will return an error if no options are supported.
						Ok((
							list.span(),
							trait_.parse_derive_trait(attrs, meta.span(), Some(nested))?,
						))
					}
					Meta::NameValue(name_value) => Err(Error::option_syntax(name_value.span())),
				}
			}
			Err(error) => Err(Error::trait_syntax(error.span())),
		}
	}
}

/// Single trait implementation. Parses attributes and constructs `impl`s.
pub trait TraitImpl: Deref<Target = Trait> {
	/// [`str`] representation of this [`Trait`].
	/// Used to compare against [`Ident`](struct@syn::Ident)s and create error
	/// messages.
	fn as_str() -> &'static str
	where
		Self: Sized;

	/// Associated [`DeriveTrait`].
	fn default_derive_trait() -> DeriveTrait
	where
		Self: Sized;

	/// Parse a `derive_where` trait with it's options.
	fn parse_derive_trait(
		_attrs: &[Attribute],
		span: Span,
		list: Option<Punctuated<Meta, Token![,]>>,
	) -> Result<DeriveTrait>
	where
		Self: Sized,
	{
		if list.is_some() {
			Err(Error::options(span, Self::as_str()))
		} else {
			Ok(Self::default_derive_trait())
		}
	}

	/// Returns `true` if [`Trait`] supports unions.
	fn supports_union() -> bool
	where
		Self: Sized,
	{
		false
	}

	/// Additional bounds to add to [`WhereClause`].
	fn additional_where_bounds(_data: &Item) -> Option<TypeParamBound>
	where
		Self: Sized,
	{
		None
	}

	/// Returns fully qualified [`Path`] for this trait.
	fn path(&self) -> Path;

	/// Additional implementation to add for this [`Trait`].
	fn additional_impl(&self) -> Option<(Path, TokenStream)> {
		None
	}

	/// Trait to implement. Only used by [`Eq`] and
	/// [`ZeroizeOnDrop`](https://docs.rs/zeroize/latest/zeroize/trait.ZeroizeOnDrop.html).
	#[allow(clippy::too_many_arguments)]
	fn impl_item(
		&self,
		_crate_: Option<&Path>,
		_full_item: &DeriveInput,
		imp: &ImplGenerics<'_>,
		ident: &Ident,
		ty: &TypeGenerics<'_>,
		where_clause: &Option<Cow<'_, WhereClause>>,
		body: TokenStream,
	) -> TokenStream {
		let path = self.path();

		quote! {
			#[automatically_derived]
			impl #imp #path for #ident #ty
			#where_clause
			{
				#body
			}
		}
	}

	/// Build method signature for this [`Trait`].
	fn build_signature(
		&self,
		_derive_where: &DeriveWhere,
		_item: &Item,
		_generics: &SplitGenerics<'_>,
		_body: &TokenStream,
	) -> TokenStream {
		TokenStream::new()
	}

	/// Build method body for this [`Trait`].
	fn build_body(&self, _derive_where: &DeriveWhere, _data: &Data) -> TokenStream {
		TokenStream::new()
	}
}
