use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{Result, *};

#[proc_macro_derive(FlatDebug)]
pub fn derive_flat_debug<TokenStream>(item: TokenStream) -> TokenStream
where
	TokenStream: Into<TokenStream2>,
	TokenStream2: Into<TokenStream>,
{
	try_derive_flat_debug(item.into())
		.unwrap_or_else(|err| err.to_compile_error())
		.into()
}

fn try_derive_flat_debug(input: TokenStream2) -> Result<TokenStream2> {
	let DeriveInput { ident, data, .. } = parse2(input)?;
	let input = match data {
		Data::Struct(s) => s,
		_ => {
			return Err(syn::Error::new(
				ident.span(),
				"`#[derive(FlatDebug)]` can only be used on structs",
			))
		}
	};
	let mut debug_fields = Vec::with_capacity(input.fields.len());
	for field in &input.fields {
		let field_name = field.ident.as_ref().unwrap();
		let field_name_str = field_name.to_string();
		debug_fields.push(quote! {
			dbg.field(#field_name_str, &self.#field_name);
		});
	}
	Ok(quote! {
		impl crate::debug::FlatDebug for #ident {
			fn flat_debug(&self, dbg: &mut std::fmt::DebugStruct) {
				#(#debug_fields)*
			}
		}
	})
}

#[proc_macro_attribute]
pub fn getter<TokenStream>(attrs: TokenStream, input: TokenStream) -> TokenStream
where
	TokenStream: Into<TokenStream2>,
	TokenStream2: Into<TokenStream>,
{
	try_getter(attrs.into(), input.into())
		.unwrap_or_else(|err| err.to_compile_error())
		.into()
}

fn try_getter(attrs: TokenStream2, input: TokenStream2) -> Result<TokenStream2> {
	enum GetterType {
		Copy,
		Ref(Type),
		Deref(Type),
	}

	let _: parse::Nothing = parse2(attrs)?;

	let mut input: ItemStruct = parse2(input)?;
	let mut clone_fields = Vec::with_capacity(input.fields.len());
	let mut getters = Vec::with_capacity(input.fields.len());
	let mut flat_debug_fields = Vec::with_capacity(input.fields.len());
	let mut dinguses = Vec::with_capacity(input.fields.len());
	for field in &mut input.fields {
		let mut getter_attributes = Vec::new();
		let mut var_attributes = Vec::new();
		let field_type = &field.ty;
		let mut getter_type = GetterType::Copy;
		for attr in &mut field.attrs {
			match () {
				_case if attr.path.is_ident("as_ref") => {
					let ref_ty: Type = attr.parse_args().unwrap_or_else(|_| field_type.clone());
					getter_type = GetterType::Ref(ref_ty);
				}
				_case if attr.path.is_ident("as_deref") => {
					let deref_ty: Type = attr.parse_args()?;
					getter_type = GetterType::Deref(deref_ty);
				}
				_case if attr.path.is_ident("copy") => {
					getter_type = GetterType::Copy;
				}
				_case if attr.path.is_ident("doc") => {
					getter_attributes.push(attr.clone());
				}
				_default => {
					var_attributes.push(attr.clone());
				}
			}
		}
		field.attrs = var_attributes;
		let field_name = field.ident.as_ref().unwrap();
		let getter = match getter_type {
			GetterType::Copy => quote! {
				#(#getter_attributes)*
				#[inline]
				pub fn #field_name(&self) -> #field_type {
					self.#field_name
				}
			},
			GetterType::Ref(ty) => quote! {
				#(#getter_attributes)*
				#[inline]
				pub fn #field_name(&self) -> &#ty {
					&self.#field_name
				}
			},
			GetterType::Deref(ty) => quote! {
				#(#getter_attributes)*
				#[inline]
				pub fn #field_name(&self) -> #ty {
					*self.#field_name
				}
			},
		};
		getters.push(getter);
		clone_fields.push(quote! {
			#field_name: self.#field_name.clone()
		});
		let field_name_str = field_name.to_string();
		flat_debug_fields.push(quote! {
			dbg.field(#field_name_str, &self.#field_name);
		});
		dinguses.push(quote! {
			params
				.entry(prefix.to_string())
				.or_insert_with(Vec::default)
				.push(#field_name_str.to_string());
		});
	}
	// And voilà! Emit back our modified `input`:
	let input_ident = input.ident;
	let input_name = input_ident.to_string();
	let input_fields = match input.fields {
		Fields::Named(named) => named.named,
		_ => panic!("unexpected input"),
	};
	Ok(quote!(
		#[derive(serde::Deserialize)]
		#[serde(bound = "")]
		pub struct #input_ident<Base: serde::de::DeserializeOwned + crate::shared::QueryParameter> {
			#[serde(flatten)]
			base: Base,
			#input_fields
		}

		impl<Base: serde::de::DeserializeOwned + crate::shared::QueryParameter> #input_ident<Base> {
			#(#getters)*
		}

		impl<Base: serde::de::DeserializeOwned + crate::shared::QueryParameter> crate::shared::QueryParameter for #input_ident<Base> {
			fn add_dingus(prefix: &str, params: &mut std::collections::HashMap<String, Vec<String>>) {
				Base::add_dingus(prefix, params);
				#(#dinguses)*
			}
		}

		impl<Base: serde::de::DeserializeOwned + crate::shared::QueryParameter + Clone> Clone for #input_ident<Base> {
			fn clone(&self) -> Self {
				Self {
					#(#clone_fields),*,
					base: self.base.clone(),
				}
			}
		}

		impl<Base: serde::de::DeserializeOwned + crate::shared::QueryParameter + crate::debug::FlatDebug> crate::debug::FlatDebug for #input_ident<Base> {
			fn flat_debug(&self, dbg: &mut std::fmt::DebugStruct) {
				self.base.flat_debug(dbg);
				#(#flat_debug_fields)*
			}
		}

		impl<Base: serde::de::DeserializeOwned + crate::shared::QueryParameter + crate::debug::FlatDebug> std::fmt::Debug for #input_ident<Base> {
			fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
				use crate::debug::FlatDebug;
				let mut dbg = f.debug_struct(#input_name);
				self.flat_debug(&mut dbg);
				dbg.finish()
			}
		}
	))
}
