use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream};
use quote::{format_ident, quote, TokenStreamExt};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

struct ExpectedErrors(Vec<String>);

impl quote::ToTokens for ExpectedErrors {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let mut inner = TokenStream::new();
        for err in self.0.iter() {
            inner.append(Literal::string(err));
            inner.append(Punct::new(',', Spacing::Alone));
        }

        tokens.append(Ident::new("vec", Span::call_site()));
        tokens.append(Punct::new('!', Spacing::Joint));
        tokens.append(Group::new(Delimiter::Parenthesis, inner))
    }
}

fn generate_test_for_file(path: PathBuf) -> String {
    let contents = fs::read_to_string(&path)
        .expect("Unable to read input")
        .replace("\r\n", "\n");
    let display_path = path.display().to_string();

    // Start iterating over lines and processing directives....
    let mut disabled = quote! {};
    let mut assertions = Vec::new();
    let mut lines = contents.lines();
    while let Some(line) = lines.next() {
        let line = match line.strip_prefix("//") {
            Some(line) => line,
            None => continue,
        };

        let line = line.trim();
        if let Some(line) = line.strip_prefix("@ignore") {
            let reason = line.trim();
            assert_ne!(
                reason, "",
                "You need to provide at least some description for ignoring in {display_path}"
            );
            disabled = quote! { #[ignore = #reason] };
        } else if line == "@concrete:" {
            let mut concrete = String::new();
            while let Some(line) = lines.next() {
                let line = match line.strip_prefix("// | ") {
                    Some(line) => line,
                    None => break,
                };

                concrete.push_str(line);
                concrete.push_str("\n");
            }

            assertions.push(quote! {
                crate::assert_concrete(&_tree, #concrete, #display_path);
            });
        } else if line == "@compiles-to:" {
            let mut compiled = String::new();
            while let Some(line) = lines.next() {
                let line = match line.strip_prefix("// | ") {
                    Some(line) => line,
                    None => break,
                };

                compiled.push_str(line);
                compiled.push_str("\n");
            }

            assertions.push(quote! {
                crate::assert_compiles_to(&_tree, &_lines, #compiled, #display_path);
            });
        } else if let Some(line) = line.strip_prefix("@type:") {
            let (pos, expected) = line
                .trim()
                .split_once(' ')
                .expect("Mal-formed type expectation");
            let pos: usize = pos
                .trim()
                .parse()
                .expect(&format!("Unable to parse position '{pos}'"));
            let expected = expected.trim();
            assertions.push(quote! {
                crate::assert_type_at(&_tree, &_lines, #pos, #expected, #display_path);
            });
        } else if let Some(line) = line.strip_prefix("@type-error:") {
            let (pos, expected) = line
                .trim()
                .split_once(' ')
                .expect("Mal-formed type-error expectation");
            let pos: usize = pos
                .trim()
                .parse()
                .expect(&format!("Unable to parse position '{pos}'"));
            let expected = expected.trim();
            assertions.push(quote! {
                crate::assert_type_error_at(&_tree, &_lines, #pos, #expected, #display_path);
            });
        } else if line == "@no-errors" {
            assertions.push(quote! {
                crate::assert_no_errors(&_tree, &_lines);
            });
        } else if let Some(line) = line.strip_prefix("@eval:") {
            let expected = line.trim();
            assertions.push(quote! {
                crate::assert_eval_ok(&_tree, &_lines, #expected);
            });
        } else if let Some(line) = line.strip_prefix("@check-error:") {
            let expected = line.trim();
            assertions.push(quote! {
                crate::assert_check_error(&_tree, &_lines, #expected);
            });
        } else if line == "@expect-errors:" {
            let mut errors = Vec::new();
            while let Some(line) = lines.next() {
                let line = match line.strip_prefix("// | ") {
                    Some(line) => line,
                    None => break,
                };

                errors.push(line.to_string());
            }

            let errors = ExpectedErrors(errors);
            assertions.push(quote! {
                crate::assert_errors(&_tree, &_lines, #errors);
            });
        } else if line.starts_with("@") {
            panic!("Test file {display_path} has unknown directive: {line}");
        }
    }

    let name = format_ident!("{}", path.file_stem().unwrap().to_string_lossy());
    let test_method = quote! {
        #disabled
        fn #name() {
            let (_tree, _lines) = fine::parser::parse(#contents);
            #(#assertions)*
        }
    };

    let syntax_tree = syn::parse2(test_method).unwrap();
    prettyplease::unparse(&syntax_tree)
}

fn process_directory<T>(output: &mut String, path: T)
where
    T: AsRef<Path>,
{
    let fine_ext: std::ffi::OsString = "fine".into();
    let path = path.as_ref();
    for entry in std::fs::read_dir(path).expect("Unable to read directory") {
        match entry {
            Ok(dirent) => {
                let file_type = dirent.file_type().unwrap();
                if file_type.is_dir() {
                    let file_name = dirent.file_name();
                    let file_name = file_name.to_string_lossy().to_owned();
                    output.push_str(&format!("mod {file_name} {{\n"));
                    process_directory(output, dirent.path());
                    output.push_str("}\n\n");
                } else if file_type.is_file() {
                    if dirent.path().extension() == Some(&fine_ext) {
                        output.push_str(&format!("// {}\n", dirent.path().display()));
                        output.push_str("#[test]\n");
                        output.push_str(&generate_test_for_file(dirent.path()));
                        output.push_str("\n\n");
                    }
                } else {
                    eprintln!("Skipping symlink: {}", path.display());
                }
            }
            Err(e) => eprintln!("Unable to read directory entry: {:?}", e),
        }
    }
}

fn main() {
    println!("cargo:rerun-if-changed=./tests");

    let mut test_source = String::new();
    process_directory(&mut test_source, "./tests");

    let out_dir = env::var_os("OUT_DIR").unwrap();
    let dest_path = Path::new(&out_dir).join("generated_tests.rs");
    fs::write(dest_path, test_source).unwrap();
}
