// Copyright 2024 The Fuchsia Authors
//
// Licensed under the 2-Clause BSD License <LICENSE-BSD or
// https://opensource.org/license/bsd-2-clause>, Apache License, Version 2.0
// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
// This file may not be copied, modified, or distributed except according to
// those terms.

// Sometimes we want to use lints which were added after our MSRV.
// `unknown_lints` is `warn` by default and we deny warnings in CI, so without
// this attribute, any unknown lint would cause a CI failure when testing with
// our MSRV.
#![allow(unknown_lints)]
#![deny(renamed_and_removed_lints)]
#![deny(
    anonymous_parameters,
    deprecated_in_future,
    late_bound_lifetime_arguments,
    missing_copy_implementations,
    missing_debug_implementations,
    path_statements,
    patterns_in_fns_without_body,
    rust_2018_idioms,
    trivial_numeric_casts,
    unreachable_pub,
    unsafe_op_in_unsafe_fn,
    unused_extern_crates,
    variant_size_differences
)]
#![deny(
    clippy::all,
    clippy::alloc_instead_of_core,
    clippy::arithmetic_side_effects,
    clippy::as_underscore,
    clippy::assertions_on_result_states,
    clippy::as_conversions,
    clippy::correctness,
    clippy::dbg_macro,
    clippy::decimal_literal_representation,
    clippy::get_unwrap,
    clippy::indexing_slicing,
    clippy::missing_inline_in_public_items,
    clippy::missing_safety_doc,
    clippy::obfuscated_if_else,
    clippy::perf,
    clippy::print_stdout,
    clippy::style,
    clippy::suspicious,
    clippy::todo,
    clippy::undocumented_unsafe_blocks,
    clippy::unimplemented,
    clippy::unnested_or_patterns,
    clippy::unwrap_used,
    clippy::use_debug
)]

use std::{env, fs, process::Command, str};

fn main() {
    // Avoid unnecessary re-building.
    println!("cargo:rerun-if-changed=build.rs");
    // This is necessary because changes to the list of detected Rust toolchain
    // versions will affect what `--cfg`s this script emits. Without this,
    // changes to that list have no effect on the build without running `cargo
    // clean` or similar.
    println!("cargo:rerun-if-changed=Cargo.toml");

    let version_cfgs = parse_version_cfgs_from_cargo_toml();
    let rustc_version = rustc_version();

    if rustc_version >= (Version { major: 1, minor: 77, patch: 0 }) {
        for version_cfg in &version_cfgs {
            // This tells the `unexpected_cfgs` lint to expect to see all of
            // these `cfg`s. The `cargo::` syntax was only added in 1.77, so we
            // don't emit these on earlier toolchain versions.
            println!("cargo:rustc-check-cfg=cfg({})", version_cfg.cfg_name);

            // This tells the `unexpected_cfgs` lint to expect to see `cfg`s of
            // the form `rust = "1.2.3"`. These aren't real `cfg`s, but we use
            // them in `cfg_attr(doc_cfg, doc(cfg(rust = "1.2.3")))` on items
            // that are version-gated so that the rendered Rustdoc shows which
            // Rust toolchain versions those items are available on.
            let Version { major, minor, patch } = version_cfg.version;
            println!("cargo:rustc-check-cfg=cfg(rust, values(\"{}.{}.{}\"))", major, minor, patch);
        }
        // FIXME(https://github.com/rust-lang/rust/issues/124816): Remove these
        // once they're no longer needed.
        println!("cargo:rustc-check-cfg=cfg(doc_cfg)");
        println!("cargo:rustc-check-cfg=cfg(kani)");
        println!(
            "cargo:rustc-check-cfg=cfg(__ZEROCOPY_INTERNAL_USE_ONLY_NIGHTLY_FEATURES_IN_TESTS)"
        );
        println!("cargo:rustc-check-cfg=cfg(__ZEROCOPY_INTERNAL_USE_ONLY_DEV_MODE)");
        println!("cargo:rustc-check-cfg=cfg(coverage_nightly)");
        println!("cargo:rustc-check-cfg=cfg(zerocopy_inline_always)");
    }

    for version_cfg in version_cfgs {
        if rustc_version < version_cfg.version {
            println!("cargo:rustc-cfg={}", version_cfg.cfg_name);
        }
    }
}

#[derive(Debug, Ord, PartialEq, PartialOrd, Eq)]
struct Version {
    major: usize,
    minor: usize,
    patch: usize,
}

#[derive(Debug)]
struct VersionCfg {
    version: Version,
    cfg_name: String,
}

const ITER_FIRST_NEXT_EXPECT_MSG: &str = "unreachable: a string split cannot produce 0 items";

fn parse_version_cfgs_from_cargo_toml() -> Vec<VersionCfg> {
    let cargo_toml = fs::read_to_string("Cargo.toml").expect("failed to read Cargo.toml");

    // Expect a Cargo.toml with the following format:
    //
    //   ...
    //
    //   [package.metadata.build-rs]
    //   # Comments...
    //   zerocopy-panic-in-const-fn = "1.57.0"
    //
    //   ...
    //
    //   [...]
    //
    // In other words, the following sections, in order:
    // - Arbitrary content
    // - The literal header `[package.metadata.build-rs]`
    // - Any number of:
    //   - Comments
    //   - Key/value pairs
    // - A TOML table, indicating the end of the section we care about

    const TABLE_HEADER: &str = "[package.metadata.build-rs]";

    if !cargo_toml.contains(TABLE_HEADER) {
        panic!("{}", format!("Cargo.toml does not contain `{}`", TABLE_HEADER));
    }

    // Now that we know there's at least one instance of `TABLE_HEADER`, we
    // consume the iterator until we find the text following that first
    // instance. This isn't terribly bullet-proof, but we also authored
    // `Cargo.toml`, and we'd have to mess up pretty badly to accidentally put
    // two copies of the same table header in that file.
    let mut iter = cargo_toml.split(TABLE_HEADER);
    let _prefix = iter.next().expect(ITER_FIRST_NEXT_EXPECT_MSG);
    let rest = iter.next().expect("unreachable: we already confirmed that there's a table header");

    // Scan until we find the next table section, which should start with a `[`
    // character at the beginning of a line.
    let mut iter = rest.split("\n[");
    let section = iter.next().expect("unreachable: a string split cannot produce 0 items");

    section
        .lines()
        .filter_map(|line| {
            // Parse lines of one of the following forms:
            //
            //   # Comment
            //
            //   name-of-key = "1.2.3" # Comment
            //
            // Comments on their own line are ignored, and comments after a
            // key/value pair will be stripped before further processing.

            // We don't need to handle the case where the `#` character isn't a
            // comment (which can happen if it's inside a string) since we authored
            // `Cargo.toml` and, in this section, we only put Rust version numbers
            // in strings.
            let before_comment = line.split('#').next().expect(ITER_FIRST_NEXT_EXPECT_MSG);
            let before_comment_without_whitespace = before_comment.trim_start();
            if before_comment_without_whitespace.is_empty() {
                return None;
            }

            // At this point, assuming Cargo.toml is correctly formatted according
            // to the format expected by this function, we know that
            // `before_comment_without_whitespace` is of the form:
            //
            //   name-of-key = "1.2.3" # Comment
            //
            // ...with no leading whitespace, and where the trailing comment is
            // optional.

            let mut iter = before_comment_without_whitespace.split_whitespace();
            let name = iter.next().expect(ITER_FIRST_NEXT_EXPECT_MSG);
            const EXPECT_MSG: &str =
                "expected lines of the format `name-of-key = \"1.2.3\" # Comment`";
            let equals_sign = iter.next().expect(EXPECT_MSG);
            let value = iter.next().expect(EXPECT_MSG);

            assert_eq!(equals_sign, "=", "{}", EXPECT_MSG);

            // Replace dashes with underscores.
            let name = name.replace('-', "_");

            // Strip the quotation marks.
            let value = value.trim_start_matches('"').trim_end_matches('"');

            let mut iter = value.split('.');
            let major = iter.next().expect(ITER_FIRST_NEXT_EXPECT_MSG);
            let minor = iter.next().expect(EXPECT_MSG);
            let patch = iter.next().expect(EXPECT_MSG);

            assert_eq!(iter.next(), None, "{}", EXPECT_MSG);

            let major: usize = major.parse().expect(EXPECT_MSG);
            let minor: usize = minor.parse().expect(EXPECT_MSG);
            let patch: usize = patch.parse().expect(EXPECT_MSG);

            Some(VersionCfg { version: Version { major, minor, patch }, cfg_name: name })
        })
        .collect()
}

fn rustc_version() -> Version {
    let rustc_cmd_name = env::var_os("RUSTC").expect("could not get rustc command name");
    let version =
        Command::new(rustc_cmd_name).arg("--version").output().expect("could not invoke rustc");
    if !version.status.success() {
        panic!(
            "rustc failed with status: {}\nrustc output: {}",
            version.status,
            String::from_utf8_lossy(version.stderr.as_slice())
        );
    }

    const RUSTC_EXPECT_MSG: &str = "could not parse rustc version output";
    let version = str::from_utf8(version.stdout.as_slice()).expect(RUSTC_EXPECT_MSG);
    let version = version.trim_start_matches("rustc ");
    // The version string is sometimes followed by other information such as the
    // string `-nightly` or other build information. We don't care about any of
    // that.
    let version = version
        .split(|c: char| c != '.' && !c.is_ascii_digit())
        .next()
        .expect(ITER_FIRST_NEXT_EXPECT_MSG);
    let mut iter = version.split('.');
    let major = iter.next().expect(ITER_FIRST_NEXT_EXPECT_MSG);
    let minor = iter.next().expect(RUSTC_EXPECT_MSG);
    let patch = iter.next().expect(RUSTC_EXPECT_MSG);

    let major: usize = major.parse().expect(RUSTC_EXPECT_MSG);
    let minor: usize = minor.parse().expect(RUSTC_EXPECT_MSG);
    let patch: usize = patch.parse().expect(RUSTC_EXPECT_MSG);

    Version { major, minor, patch }
}
