// Exclude lints we don't think are valuable.
#![allow(clippy::uninlined_format_args)] // Allow `format!("{}", x)` instead of enforcing `format!("{x}")`

const MAX_USED_VERSION: u64 = 80;

use std::io;

fn main() {
    let rustc = std::env::var_os("RUSTC");
    let rustc = rustc.as_ref().map_or_else(|| "rustc".as_ref(), std::path::Path::new);
    let output = std::process::Command::new(rustc)
        .arg("--version")
        .output()
        .unwrap_or_else(|error| panic!("failed to run `{:?} --version`: {:?}", rustc, error));
    assert!(output.status.success(), "{:?} -- version returned non-zero exit code", rustc);
    let stdout = String::from_utf8(output.stdout).expect("rustc produced non-UTF-8 output");
    let version_prefix = "rustc ";
    assert!(stdout.starts_with(version_prefix), "unexpected rustc output: {}", stdout);

    let version = &stdout[version_prefix.len()..];
    let end = version.find(&[' ', '-'] as &[_]).unwrap_or(version.len());
    let version = &version[..end];
    let mut version_components = version.split('.');
    let major = version_components.next().unwrap();
    assert_eq!(major, "1", "unexpected Rust major version");
    let minor = version_components
        .next()
        .unwrap_or("0")
        .parse::<u64>()
        .expect("invalid Rust minor version");

    let msrv = std::env::var("CARGO_PKG_RUST_VERSION").unwrap();
    let mut msrv = msrv.split('.');
    let msrv_major = msrv.next().unwrap();
    assert_eq!(msrv_major, "1", "unexpected Rust major version");
    let msrv_minor = msrv.next().unwrap().parse::<u64>().unwrap();

    let out_dir = std::env::var_os("OUT_DIR").expect("missing OUT_DIR env var");
    let out_dir = std::path::PathBuf::from(out_dir);
    let macro_file = std::fs::File::create(out_dir.join("rust_version.rs"))
        .expect("failed to create rust_version.rs");
    let macro_file = io::BufWriter::new(macro_file);
    write_macro(macro_file, msrv_minor, minor).expect("failed to write to rust_version.rs");
}

fn write_macro(mut macro_file: impl io::Write, msrv_minor: u64, minor: u64) -> io::Result<()> {
    writeln!(macro_file, "/// Expands code based on Rust version this is compiled under.")?;
    writeln!(macro_file, "///")?;
    writeln!(macro_file, "/// Example:")?;
    writeln!(macro_file, "/// ```")?;
    writeln!(macro_file, "/// bitcoin_internals::rust_version! {{")?;
    writeln!(macro_file, "///     if >= 1.78 {{")?;
    writeln!(macro_file, "///         println!(\"This is Rust 1.78+\");")?;
    writeln!(macro_file, "///     }} else {{")?;
    writeln!(macro_file, "///         println!(\"This is Rust < 1.78\");")?;
    writeln!(macro_file, "///     }}")?;
    writeln!(macro_file, "/// }}")?;
    writeln!(macro_file, "/// ```")?;
    writeln!(macro_file, "///")?;
    writeln!(macro_file, "/// The `else` branch is optional.")?;
    writeln!(macro_file, "/// Currently only the `>=` operator is supported.")?;
    writeln!(macro_file, "#[macro_export]")?;
    writeln!(macro_file, "macro_rules! rust_version {{")?;

    // These two loops are the magic; we output the clause if_yes/if_no
    // dependent on the current compiler version (`minor`).
    for version in msrv_minor..=minor {
        writeln!(
            macro_file,
            "    (if >= 1.{} {{ $($if_yes:tt)* }} $(else {{ $($if_no:tt)* }})?) => {{",
            version
        )?;
        writeln!(macro_file, "        $($if_yes)*")?;
        writeln!(macro_file, "    }};")?;
    }
    for version in (minor + 1)..=MAX_USED_VERSION {
        writeln!(
            macro_file,
            "    (if >= 1.{} {{ $($if_yes:tt)* }} $(else {{ $($if_no:tt)* }})?) => {{",
            version
        )?;
        writeln!(macro_file, "        $($($if_no)*)?")?;
        writeln!(macro_file, "    }};")?;
    }
    writeln!(macro_file, "    (if >= $unknown:tt $($rest:tt)*) => {{")?;
    writeln!(
        macro_file,
        "        compile_error!(concat!(\"unknown Rust version \", stringify!($unknown)));"
    )?;
    writeln!(macro_file, "    }};")?;
    writeln!(macro_file, "}}")?;
    writeln!(macro_file, "pub use rust_version;")?;
    macro_file.flush()
}
