use std::env;

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

    #[cfg(feature = "musl-reference-tests")]
    musl_reference_tests::generate();

    if !cfg!(feature = "checked") {
        let lvl = env::var("OPT_LEVEL").unwrap();
        if lvl != "0" {
            println!("cargo:rustc-cfg=assert_no_panic");
        }
    }
}

#[cfg(feature = "musl-reference-tests")]
mod musl_reference_tests {
    use rand::seq::SliceRandom;
    use rand::Rng;
    use std::env;
    use std::fs;
    use std::process::Command;

    // Number of tests to generate for each function
    const NTESTS: usize = 500;

    // These files are all internal functions or otherwise miscellaneous, not
    // defining a function we want to test.
    const IGNORED_FILES: &[&str] = &[
        "fenv.rs",
        // These are giving slightly different results compared to musl
        "lgamma.rs",
        "lgammaf.rs",
        "tgamma.rs",
        "j0.rs",
        "j0f.rs",
        "jn.rs",
        "jnf.rs",
        "j1.rs",
        "j1f.rs",
    ];

    struct Function {
        name: String,
        args: Vec<Ty>,
        ret: Vec<Ty>,
        tests: Vec<Test>,
    }

    enum Ty {
        F32,
        F64,
        I32,
        Bool,
    }

    struct Test {
        inputs: Vec<i64>,
        outputs: Vec<i64>,
    }

    pub fn generate() {
        // PowerPC tests are failing on LLVM 13: https://github.com/rust-lang/rust/issues/88520
        let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
        if target_arch == "powerpc64" {
            return;
        }

        let files = fs::read_dir("src/math")
            .unwrap()
            .map(|f| f.unwrap().path())
            .collect::<Vec<_>>();

        let mut math = Vec::new();
        for file in files {
            if IGNORED_FILES.iter().any(|f| file.ends_with(f)) {
                continue;
            }

            println!("generating musl reference tests in {:?}", file);

            let contents = fs::read_to_string(file).unwrap();
            let mut functions = contents.lines().filter(|f| f.starts_with("pub fn"));
            while let Some(function_to_test) = functions.next() {
                math.push(parse(function_to_test));
            }
        }

        // Generate a bunch of random inputs for each function. This will
        // attempt to generate a good set of uniform test cases for exercising
        // all the various functionality.
        generate_random_tests(&mut math, &mut rand::thread_rng());

        // After we have all our inputs, use the x86_64-unknown-linux-musl
        // target to generate the expected output.
        generate_test_outputs(&mut math);
        //panic!("Boo");
        // ... and now that we have both inputs and expected outputs, do a bunch
        // of codegen to create the unit tests which we'll actually execute.
        generate_unit_tests(&math);
    }

    /// A "poor man's" parser for the signature of a function
    fn parse(s: &str) -> Function {
        let s = eat(s, "pub fn ");
        let pos = s.find('(').unwrap();
        let name = &s[..pos];
        let s = &s[pos + 1..];
        let end = s.find(')').unwrap();
        let args = s[..end]
            .split(',')
            .map(|arg| {
                let colon = arg.find(':').unwrap();
                parse_ty(arg[colon + 1..].trim())
            })
            .collect::<Vec<_>>();
        let tail = &s[end + 1..];
        let tail = eat(tail, " -> ");
        let ret = parse_retty(tail.replace("{", "").trim());

        return Function {
            name: name.to_string(),
            args,
            ret,
            tests: Vec::new(),
        };

        fn parse_ty(s: &str) -> Ty {
            match s {
                "f32" => Ty::F32,
                "f64" => Ty::F64,
                "i32" => Ty::I32,
                "bool" => Ty::Bool,
                other => panic!("unknown type `{}`", other),
            }
        }

        fn parse_retty(s: &str) -> Vec<Ty> {
            match s {
                "(f32, f32)" => vec![Ty::F32, Ty::F32],
                "(f32, i32)" => vec![Ty::F32, Ty::I32],
                "(f64, f64)" => vec![Ty::F64, Ty::F64],
                "(f64, i32)" => vec![Ty::F64, Ty::I32],
                other => vec![parse_ty(other)],
            }
        }

        fn eat<'a>(s: &'a str, prefix: &str) -> &'a str {
            if s.starts_with(prefix) {
                &s[prefix.len()..]
            } else {
                panic!("{:?} didn't start with {:?}", s, prefix)
            }
        }
    }

    fn generate_random_tests<R: Rng>(functions: &mut [Function], rng: &mut R) {
        for function in functions {
            for _ in 0..NTESTS {
                function.tests.push(generate_test(function, rng));
            }
        }

        fn generate_test<R: Rng>(function: &Function, rng: &mut R) -> Test {
            let mut inputs = function
                .args
                .iter()
                .map(|ty| ty.gen_i64(rng))
                .collect::<Vec<_>>();

            // First argument to this function appears to be a number of
            // iterations, so passing in massive random numbers causes it to
            // take forever to execute, so make sure we're not running random
            // math code until the heat death of the universe.
            if function.name == "jn" || function.name == "jnf" {
                inputs[0] &= 0xffff;
            }

            Test {
                inputs,
                // zero output for now since we'll generate it later
                outputs: vec![],
            }
        }
    }

    impl Ty {
        fn gen_i64<R: Rng>(&self, r: &mut R) -> i64 {
            use std::f32;
            use std::f64;

            return match self {
                Ty::F32 => {
                    if r.gen_range(0, 20) < 1 {
                        let i = *[f32::NAN, f32::INFINITY, f32::NEG_INFINITY]
                            .choose(r)
                            .unwrap();
                        i.to_bits().into()
                    } else {
                        r.gen::<f32>().to_bits().into()
                    }
                }
                Ty::F64 => {
                    if r.gen_range(0, 20) < 1 {
                        let i = *[f64::NAN, f64::INFINITY, f64::NEG_INFINITY]
                            .choose(r)
                            .unwrap();
                        i.to_bits() as i64
                    } else {
                        r.gen::<f64>().to_bits() as i64
                    }
                }
                Ty::I32 => {
                    if r.gen_range(0, 10) < 1 {
                        let i = *[i32::max_value(), 0, i32::min_value()].choose(r).unwrap();
                        i.into()
                    } else {
                        r.gen::<i32>().into()
                    }
                }
                Ty::Bool => r.gen::<bool>() as i64,
            };
        }

        fn libc_ty(&self) -> &'static str {
            match self {
                Ty::F32 => "f32",
                Ty::F64 => "f64",
                Ty::I32 => "i32",
                Ty::Bool => "i32",
            }
        }

        fn libc_pty(&self) -> &'static str {
            match self {
                Ty::F32 => "*mut f32",
                Ty::F64 => "*mut f64",
                Ty::I32 => "*mut i32",
                Ty::Bool => "*mut i32",
            }
        }

        fn default(&self) -> &'static str {
            match self {
                Ty::F32 => "0_f32",
                Ty::F64 => "0_f64",
                Ty::I32 => "0_i32",
                Ty::Bool => "false",
            }
        }

        fn to_i64(&self) -> &'static str {
            match self {
                Ty::F32 => ".to_bits() as i64",
                Ty::F64 => ".to_bits() as i64",
                Ty::I32 => " as i64",
                Ty::Bool => " as i64",
            }
        }
    }

    fn generate_test_outputs(functions: &mut [Function]) {
        let mut src = String::new();
        let dst = std::env::var("OUT_DIR").unwrap();

        // Generate a program which will run all tests with all inputs in
        // `functions`. This program will write all outputs to stdout (in a
        // binary format).
        src.push_str("use std::io::Write;");
        src.push_str("fn main() {");
        src.push_str("let mut result = Vec::new();");
        for function in functions.iter_mut() {
            src.push_str("unsafe {");
            src.push_str("extern { fn ");
            src.push_str(&function.name);
            src.push_str("(");

            let (ret, retptr) = match function.name.as_str() {
                "sincos" | "sincosf" => (None, &function.ret[..]),
                _ => (Some(&function.ret[0]), &function.ret[1..]),
            };
            for (i, arg) in function.args.iter().enumerate() {
                src.push_str(&format!("arg{}: {},", i, arg.libc_ty()));
            }
            for (i, ret) in retptr.iter().enumerate() {
                src.push_str(&format!("argret{}: {},", i, ret.libc_pty()));
            }
            src.push_str(")");
            if let Some(ty) = ret {
                src.push_str(" -> ");
                src.push_str(ty.libc_ty());
            }
            src.push_str("; }");

            src.push_str(&format!("static TESTS: &[[i64; {}]]", function.args.len()));
            src.push_str(" = &[");
            for test in function.tests.iter() {
                src.push_str("[");
                for val in test.inputs.iter() {
                    src.push_str(&val.to_string());
                    src.push_str(",");
                }
                src.push_str("],");
            }
            src.push_str("];");

            src.push_str("for test in TESTS {");
            for (i, arg) in retptr.iter().enumerate() {
                src.push_str(&format!("let mut argret{} = {};", i, arg.default()));
            }
            src.push_str("let output = ");
            src.push_str(&function.name);
            src.push_str("(");
            for (i, arg) in function.args.iter().enumerate() {
                src.push_str(&match arg {
                    Ty::F32 => format!("f32::from_bits(test[{}] as u32)", i),
                    Ty::F64 => format!("f64::from_bits(test[{}] as u64)", i),
                    Ty::I32 => format!("test[{}] as i32", i),
                    Ty::Bool => format!("test[{}] as i32", i),
                });
                src.push_str(",");
            }
            for (i, _) in retptr.iter().enumerate() {
                src.push_str(&format!("&mut argret{},", i));
            }
            src.push_str(");");
            if let Some(ty) = &ret {
                src.push_str(&format!("let output = output{};", ty.to_i64()));
                src.push_str("result.extend_from_slice(&output.to_le_bytes());");
            }

            for (i, ret) in retptr.iter().enumerate() {
                src.push_str(&format!(
                    "result.extend_from_slice(&(argret{}{}).to_le_bytes());",
                    i,
                    ret.to_i64(),
                ));
            }
            src.push_str("}");

            src.push_str("}");
        }

        src.push_str("std::io::stdout().write_all(&result).unwrap();");

        src.push_str("}");

        let path = format!("{}/gen.rs", dst);
        fs::write(&path, src).unwrap();

        // Make it somewhat pretty if something goes wrong
        drop(Command::new("rustfmt").arg(&path).status());

        // Compile and execute this tests for the musl target, assuming we're an
        // x86_64 host effectively.
        let status = Command::new("rustc")
            .current_dir(&dst)
            .arg(&path)
            .arg("--target=x86_64-unknown-linux-musl")
            .status()
            .unwrap();
        assert!(status.success());
        let output = Command::new("./gen").current_dir(&dst).output().unwrap();
        assert!(output.status.success());
        assert!(output.stderr.is_empty());

        // Map all the output bytes back to an `i64` and then shove it all into
        // the expected results.
        let mut results = output.stdout.chunks_exact(8).map(|buf| {
            let mut exact = [0; 8];
            exact.copy_from_slice(buf);
            i64::from_le_bytes(exact)
        });

        for f in functions.iter_mut() {
            for test in f.tests.iter_mut() {
                test.outputs = (0..f.ret.len()).map(|_| results.next().unwrap()).collect();
            }
        }
        assert!(results.next().is_none());
    }

    /// Codegens a file which has a ton of `#[test]` annotations for all the
    /// tests that we generated above.
    fn generate_unit_tests(functions: &[Function]) {
        let mut src = String::new();
        let dst = std::env::var("OUT_DIR").unwrap();

        for function in functions {
            src.push_str("#[test]");
            src.push_str("fn ");
            src.push_str(&function.name);
            src.push_str("_matches_musl() {");
            src.push_str(&format!(
                "static TESTS: &[([i64; {}], [i64; {}])]",
                function.args.len(),
                function.ret.len(),
            ));
            src.push_str(" = &[");
            for test in function.tests.iter() {
                src.push_str("([");
                for val in test.inputs.iter() {
                    src.push_str(&val.to_string());
                    src.push_str(",");
                }
                src.push_str("],");
                src.push_str("[");
                for val in test.outputs.iter() {
                    src.push_str(&val.to_string());
                    src.push_str(",");
                }
                src.push_str("],");
                src.push_str("),");
            }
            src.push_str("];");

            src.push_str("for (test, expected) in TESTS {");
            src.push_str("let output = ");
            src.push_str(&function.name);
            src.push_str("(");
            for (i, arg) in function.args.iter().enumerate() {
                src.push_str(&match arg {
                    Ty::F32 => format!("f32::from_bits(test[{}] as u32)", i),
                    Ty::F64 => format!("f64::from_bits(test[{}] as u64)", i),
                    Ty::I32 => format!("test[{}] as i32", i),
                    Ty::Bool => format!("test[{}] as i32", i),
                });
                src.push_str(",");
            }
            src.push_str(");");

            for (i, ret) in function.ret.iter().enumerate() {
                let get = if function.ret.len() == 1 {
                    String::new()
                } else {
                    format!(".{}", i)
                };
                src.push_str(&(match ret {
                    Ty::F32 => format!("if _eqf(output{}, f32::from_bits(expected[{}] as u32)).is_ok() {{ continue }}", get, i),
                    Ty::F64 => format!("if _eq(output{}, f64::from_bits(expected[{}] as u64)).is_ok() {{ continue }}", get, i),
                    Ty::I32 => format!("if output{} as i64 == expected[{}] {{ continue }}", get, i),
                    Ty::Bool => unreachable!(),
                }));
            }

            src.push_str(
                r#"
                panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", test, expected, output);
            "#,
            );
            src.push_str("}");

            src.push_str("}");
        }

        let path = format!("{}/musl-tests.rs", dst);
        fs::write(&path, src).unwrap();

        // Try to make it somewhat pretty
        drop(Command::new("rustfmt").arg(&path).status());
    }
}
