#[cfg(proc_macro)]
extern crate proc_macro;

use proc_macro2::TokenStream;
use quote::quote;
use std::path::Path;
use syn::*;

mod ignores;

#[cfg(proc_macro)]
#[proc_macro_attribute]
pub fn compiler_test(
    attrs: proc_macro::TokenStream,
    input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    compiler_test_impl(attrs.into(), input.into()).into()
}

fn compiler_test_impl(attrs: TokenStream, input: TokenStream) -> TokenStream {
    let path: Option<ExprPath> = parse2::<ExprPath>(attrs).ok();
    let mut my_fn: ItemFn = match syn::parse2(input) {
        Ok(f) => f,
        Err(e) => return e.into_compile_error(),
    };
    let fn_name = &my_fn.sig.ident;

    // Let's build the ignores to append an `#[ignore]` macro to the
    // autogenerated tests in case the test appears in the `ignores.txt` path;

    let tests_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
        .ancestors()
        .nth(2)
        .unwrap();
    let ignores_txt_path = tests_dir.join("ignores.txt");

    let ignores = crate::ignores::Ignores::build_from_path(ignores_txt_path);

    let should_ignore = |test_name: &str, compiler_name: &str, engine_name: &str| {
        let compiler_name = compiler_name.to_lowercase();
        let engine_name = engine_name.to_lowercase();
        // We construct the path manually because we can't get the
        // source_file location from the `Span` (it's only available in nightly)
        let full_path = format!(
            "{}::{}::{}::{}",
            quote! { #path },
            test_name,
            compiler_name,
            engine_name
        )
        .replace(' ', "");

        // println!("{} -> Should ignore: {}", full_path, should_ignore);
        ignores.should_ignore_host(&engine_name, &compiler_name, &full_path)
    };
    let construct_engine_test = |func: &::syn::ItemFn,
                                 compiler_name: &str,
                                 engine_name: &str,
                                 engine_feature_name: &str|
     -> ::proc_macro2::TokenStream {
        let config_compiler = ::quote::format_ident!("{}", compiler_name);
        let test_name = ::quote::format_ident!("{}", engine_name.to_lowercase());
        let mut new_sig = func.sig.clone();
        let attrs = func
            .attrs
            .clone()
            .iter()
            .fold(quote! {}, |acc, new| quote! {#acc #new});
        new_sig.ident = test_name;
        new_sig.inputs = ::syn::punctuated::Punctuated::new();
        let f = quote! {
            #[test_log::test]
            #attrs
            #[cfg(feature = #engine_feature_name)]
            #new_sig {
                #fn_name(crate::Config::new(crate::Compiler::#config_compiler))
            }
        };
        if should_ignore(
            &func.sig.ident.to_string().replace("r#", ""),
            compiler_name,
            engine_name,
        ) && !cfg!(test)
        {
            quote! {
                #[ignore]
                #f
            }
        } else {
            f
        }
    };

    let construct_compiler_test =
        |func: &::syn::ItemFn, compiler_name: &str| -> ::proc_macro2::TokenStream {
            let mod_name = ::quote::format_ident!("{}", compiler_name.to_lowercase());
            let universal_engine_test = construct_engine_test(
                func,
                compiler_name,
                compiler_name,
                &compiler_name.to_lowercase(),
            );
            let compiler_name_lowercase = compiler_name.to_lowercase();

            quote! {
                #[cfg(feature = #compiler_name_lowercase)]
                mod #mod_name {
                    use super::*;

                    #universal_engine_test
                }
            }
        };

    let singlepass_compiler_test = construct_compiler_test(&my_fn, "Singlepass");
    let cranelift_compiler_test = construct_compiler_test(&my_fn, "Cranelift");
    let llvm_compiler_test = construct_compiler_test(&my_fn, "LLVM");

    // We remove the method decorators
    my_fn.attrs = vec![];

    let x = quote! {
        #[cfg(test)]
        mod #fn_name {
            use super::*;

            #[allow(unused)]
            #my_fn

            #singlepass_compiler_test
            #cranelift_compiler_test
            #llvm_compiler_test
        }
    };

    #[allow(clippy::useless_conversion)]
    x.into()
}

#[cfg(test)]
mod tests;
