mod macro_args;

use macro_args::TraitAndImpls;
use proc_macro2::TokenStream;
use proc_macro_error::{emit_call_site_error, proc_macro_error};
use quote::format_ident;
use quote::quote;
use syn::ItemFn;

/// Takes the original function and repeats it for each of the implementations provided. Example:
/// ```
/// #[test]
/// fn do_test() {
///     ExampleTrait::do_thing()
/// }
/// ```
/// becomes:
/// ```
/// #[test]
/// fn do_test() {
///     fn impl_ExampleStruct() {
///         type ExampleTrait = ExampleStruct;
///         ExampleTrait::do_thing();
///     }
///     impl_ExampleStruct();
///
///     fn impl_ExampleStruct2() {
///         type ExampleTrait = ExampleStruct2;
///         ExampleTrait::do_thing();
///     }
///     impl_ExampleStruct2();
/// }
///
fn repeat_function_with_mappings(func: &ItemFn, trait_and_impls: TraitAndImpls) -> TokenStream {
    let impl_blocks: Vec<TokenStream> = trait_and_impls
        .structs
        .iter()
        .map(|struc| {
            let fn_ident = format_ident!("impl_{}", struc.ident);
            let trait_ident = &trait_and_impls.base_trait.ident;
            let trait_generics = &trait_and_impls.base_trait.generics;

            let struct_ident = &struc.ident;
            let struct_generics = &struc.generics;
            let stmts = &func.block.stmts;

            quote! {
                fn #fn_ident() {
                    type #trait_ident#trait_generics = #struct_ident#struct_generics;
                    #(#stmts)*
                }

                #fn_ident();
            }
        })
        .collect();

    let attrs = &func.attrs;
    let vis = &func.vis;
    let sig = &func.sig;

    quote! {
        #(#attrs)*
        #[allow(non_snake_case)]
        #vis #sig
        {
            #(#impl_blocks)*
        }
    }
}

/// Run this test multiple times, replacing all references to the trait specified with a specific implementation.
/// Use it like this:
///
/// `#[test_impl(ExampleTrait = ExampleStruct, ExampleStruct2)]`
#[proc_macro_attribute]
#[proc_macro_error]
pub fn test_impl(
    args: proc_macro::TokenStream,
    input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let args = match syn::parse::<TraitAndImpls>(args) {
        Ok(a) => a,
        Err(e) => {
            emit_call_site_error!("Could not parse macro arguments: {}", e);
            return proc_macro::TokenStream::new();
        }
    };

    let fn_def = match syn::parse::<ItemFn>(input) {
        Ok(f) => f,
        Err(e) => {
            emit_call_site_error!("You must use this macro with a function: {}", e);
            return proc_macro::TokenStream::new();
        }
    };

    let impl_checks = args.output_impl_checks(&fn_def.sig.ident);
    let mapped = repeat_function_with_mappings(&fn_def, args);
    (quote! {
        #impl_checks
        #mapped
    })
    .into()
}