// Copyright (c) Microsoft Corporation
// License: MIT OR Apache-2.0

//! Build script for the `wdk-sys` crate.
//!
//! This parses the WDK configuration from metadata provided in the build tree,
//! and generates the relevant bindings to WDK APIs.

use std::{
    env,
    fs::File,
    io::Write,
    panic,
    path::{Path, PathBuf},
    sync::LazyLock,
    thread,
};

use anyhow::Context;
use bindgen::CodegenConfig;
use tracing::{Span, info, info_span, trace};
use tracing_subscriber::{
    EnvFilter,
    filter::{LevelFilter, ParseError},
};
use wdk_build::{
    ApiSubset,
    BuilderExt,
    Config,
    ConfigError,
    DriverConfig,
    IoError,
    KmdfConfig,
    UmdfConfig,
    configure_wdk_library_build_and_then,
};

const OUT_DIR_PLACEHOLDER: &str =
    "<PLACEHOLDER FOR LITERAL VALUE CONTAINING OUT_DIR OF wdk-sys CRATE>";
const WDFFUNCTIONS_SYMBOL_NAME_PLACEHOLDER: &str =
    "<PLACEHOLDER FOR LITERAL VALUE CONTAINING WDFFUNCTIONS SYMBOL NAME>";
const WDF_FUNCTION_COUNT_PLACEHOLDER: &str =
    "<PLACEHOLDER FOR EXPRESSION FOR NUMBER OF WDF FUNCTIONS IN `wdk_sys::WdfFunctions`";

const WDF_FUNCTION_COUNT_DECLARATION_EXTERNAL_SYMBOL: &str =
    "// SAFETY: `crate::WdfFunctionCount` is generated as a mutable static, but is not supposed \
     to be ever mutated by WDF.
    (unsafe { crate::WdfFunctionCount }) as usize";

const WDF_FUNCTION_COUNT_DECLARATION_TABLE_INDEX: &str =
    "crate::_WDFFUNCENUM::WdfFunctionTableNumEntries as usize";

static WDF_FUNCTION_COUNT_FUNCTION_TEMPLATE: LazyLock<String> = LazyLock::new(|| {
    format!(
        r"#[allow(clippy::must_use_candidate)]
/// Returns the number of functions available in the WDF function table.
/// Should not be used in public API.
pub fn get_wdf_function_count() -> usize {{
    {WDF_FUNCTION_COUNT_PLACEHOLDER}
}}"
    )
});

static CALL_UNSAFE_WDF_BINDING_TEMPLATE: LazyLock<String> = LazyLock::new(|| {
    format!(
        r#"
/// A procedural macro that allows WDF functions to be called by name.
///
/// This function parses the name of the WDF function, finds it function
/// pointer from the WDF function table, and then calls it with the
/// arguments passed to it
///
/// # Safety
/// Function arguments must abide by any rules outlined in the WDF
/// documentation. This macro does not perform any validation of the
/// arguments passed to it., beyond type validation.
///
/// # Examples
///
/// ```rust, no_run
/// use wdk_sys::*;
/// 
/// pub unsafe extern "system" fn driver_entry(
///     driver: &mut DRIVER_OBJECT,
///     registry_path: PCUNICODE_STRING,
/// ) -> NTSTATUS {{
/// 
///     let mut driver_config = WDF_DRIVER_CONFIG {{
///         Size: core::mem::size_of::<WDF_DRIVER_CONFIG>() as ULONG,
///         ..WDF_DRIVER_CONFIG::default()
///     }};
///     let driver_handle_output = WDF_NO_HANDLE as *mut WDFDRIVER;
///
///     unsafe {{
///         call_unsafe_wdf_function_binding!(
///             WdfDriverCreate,
///             driver as PDRIVER_OBJECT,
///             registry_path,
///             WDF_NO_OBJECT_ATTRIBUTES,
///             &mut driver_config,
///             driver_handle_output,
///         )
///     }}
/// }}
/// ```
#[macro_export]
macro_rules! call_unsafe_wdf_function_binding {{
    ( $($tt:tt)* ) => {{
        $crate::__proc_macros::call_unsafe_wdf_function_binding! (
            r"{OUT_DIR_PLACEHOLDER}",
            $($tt)*
        )
    }}
}}"#
    )
});

static TEST_STUBS_TEMPLATE: LazyLock<String> = LazyLock::new(|| {
    format!(
        r"
use crate::WDFFUNC;

/// Stubbed version of the symbol that `WdfFunctions` links to so that test targets will compile
// SAFETY: Generated WDF symbol name is required for test compilation and is unique per build.
// No other symbols in this crate export this name, preventing linker conflicts.
#[unsafe(no_mangle)]
pub static mut {WDFFUNCTIONS_SYMBOL_NAME_PLACEHOLDER}: *const WDFFUNC = core::ptr::null();
",
    )
});

/// Enabled API subsets based off of cargo-features
const ENABLED_API_SUBSETS: &[ApiSubset] = &[
    ApiSubset::Base,
    ApiSubset::Wdf,
    #[cfg(feature = "gpio")]
    ApiSubset::Gpio,
    #[cfg(feature = "hid")]
    ApiSubset::Hid,
    #[cfg(feature = "parallel-ports")]
    ApiSubset::ParallelPorts,
    #[cfg(feature = "spb")]
    ApiSubset::Spb,
    #[cfg(feature = "storage")]
    ApiSubset::Storage,
    #[cfg(feature = "usb")]
    ApiSubset::Usb,
];

type GenerateFn = fn(&Path, &Config) -> Result<(), ConfigError>;
const BINDGEN_FILE_GENERATORS_TUPLES: &[(&str, GenerateFn)] = &[
    ("constants.rs", generate_constants),
    ("types.rs", generate_types),
    ("base.rs", generate_base),
    ("wdf.rs", generate_wdf),
    #[cfg(feature = "gpio")]
    ("gpio.rs", generate_gpio),
    #[cfg(feature = "hid")]
    ("hid.rs", generate_hid),
    #[cfg(feature = "parallel-ports")]
    ("parallel_ports.rs", generate_parallel_ports),
    #[cfg(feature = "spb")]
    ("spb.rs", generate_spb),
    #[cfg(feature = "storage")]
    ("storage.rs", generate_storage),
    #[cfg(feature = "usb")]
    ("usb.rs", generate_usb),
    #[cfg(feature = "iddcx")]
    ("iddcx.rs", generate_iddcx),
];

fn initialize_tracing() -> Result<(), ParseError> {
    let tracing_filter = EnvFilter::default()
        // Show up to INFO level by default
        .add_directive(LevelFilter::INFO.into())
        // Silence various warnings originating from bindgen that are not currently actionable
        // FIXME: this currently sets the minimum log level to error for the listed modules. It
        // should actually be turning off logging (level=off) for specific warnings in these
        // modules, but a bug in the tracing crate's filtering is preventing this from working as expected. See https://github.com/tokio-rs/tracing/issues/2843.
        .add_directive("bindgen::codegen::helpers[{message}]=error".parse()?)
        .add_directive("bindgen::codegen::struct_layout[{message}]=error".parse()?)
        .add_directive("bindgen::ir::comp[{message}]=error".parse()?)
        .add_directive("bindgen::ir::context[{message}]=error".parse()?)
        .add_directive("bindgen::ir::ty[{message}]=error".parse()?)
        .add_directive("bindgen::ir::var[{message}]=error".parse()?);

    // Allow overriding tracing behaviour via `EnvFilter::DEFAULT_ENV` env var
    let tracing_filter =
        if let Ok(filter_directives_from_env_var) = env::var(EnvFilter::DEFAULT_ENV) {
            // Append each directive from the env var to the filter
            filter_directives_from_env_var.split(',').fold(
                tracing_filter,
                |tracing_filter, filter_directive| {
                    match filter_directive.parse() {
                        Ok(parsed_filter_directive) => {
                            tracing_filter.add_directive(parsed_filter_directive)
                        }
                        Err(parsing_error) => {
                            // Must use eprintln!() here as tracing is not yet initialized
                            eprintln!(
                                "Skipping filter directive, {}, which failed to be parsed from {} \
                                 obtained from {} with the following error: {}",
                                filter_directive,
                                filter_directives_from_env_var,
                                EnvFilter::DEFAULT_ENV,
                                parsing_error
                            );
                            tracing_filter
                        }
                    }
                },
            )
        } else {
            tracing_filter
        };

    tracing_subscriber::fmt()
        .pretty()
        .with_env_filter(tracing_filter)
        .with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE)
        .init();

    Ok(())
}

fn generate_constants(out_path: &Path, config: &Config) -> Result<(), ConfigError> {
    info!("Generating bindings to WDK: constants.rs");

    let header_contents = config.bindgen_header_contents(ENABLED_API_SUBSETS.iter().copied())?;
    trace!(header_contents = ?header_contents);

    let bindgen_builder = bindgen::Builder::wdk_default(config)?
        .with_codegen_config(CodegenConfig::VARS)
        .header_contents("constants-input.h", &header_contents);
    trace!(bindgen_builder = ?bindgen_builder);

    let output_file_path = out_path.join("constants.rs");
    Ok(bindgen_builder
        .generate()
        .expect("Bindings should succeed to generate")
        .write_to_file(&output_file_path)
        .map_err(|source| IoError::with_path(output_file_path, source))?)
}

fn generate_types(out_path: &Path, config: &Config) -> Result<(), ConfigError> {
    info!("Generating bindings to WDK: types.rs");

    let header_contents = config.bindgen_header_contents(ENABLED_API_SUBSETS.iter().copied())?;
    trace!(header_contents = ?header_contents);

    let bindgen_builder = bindgen::Builder::wdk_default(config)?
        .with_codegen_config(CodegenConfig::TYPES)
        .header_contents("types-input.h", &header_contents);
    trace!(bindgen_builder = ?bindgen_builder);

    let output_file_path = out_path.join("types.rs");
    Ok(bindgen_builder
        .generate()
        .expect("Bindings should succeed to generate")
        .write_to_file(&output_file_path)
        .map_err(|source| IoError::with_path(output_file_path, source))?)
}

fn generate_base(out_path: &Path, config: &Config) -> Result<(), ConfigError> {
    let outfile_name = match &config.driver_config {
        DriverConfig::Wdm | DriverConfig::Kmdf(_) => "ntddk",
        DriverConfig::Umdf(_) => "windows",
    };
    info!("Generating bindings to WDK: {outfile_name}.rs");

    let header_contents = config.bindgen_header_contents([ApiSubset::Base])?;
    trace!(header_contents = ?header_contents);

    let bindgen_builder = bindgen::Builder::wdk_default(config)?
        .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement())
        .header_contents(&format!("{outfile_name}-input.h"), &header_contents);
    trace!(bindgen_builder = ?bindgen_builder);

    let output_file_path = out_path.join(format!("{outfile_name}.rs"));
    Ok(bindgen_builder
        .generate()
        .expect("Bindings should succeed to generate")
        .write_to_file(&output_file_path)
        .map_err(|source| IoError::with_path(output_file_path, source))?)
}

fn generate_wdf(out_path: &Path, config: &Config) -> Result<(), ConfigError> {
    if let DriverConfig::Kmdf(_) | DriverConfig::Umdf(_) = config.driver_config {
        info!("Generating bindings to WDK: wdf.rs");

        let header_contents = config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf])?;
        trace!(header_contents = ?header_contents);

        let bindgen_builder = bindgen::Builder::wdk_default(config)?
            .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement())
            .header_contents("wdf-input.h", &header_contents)
            // Only generate for files that are prefixed with (case-insensitive) wdf (ie.
            // /some/path/WdfSomeHeader.h), to prevent duplication of code in ntddk.rs
            .allowlist_file("(?i).*wdf.*");
        trace!(bindgen_builder = ?bindgen_builder);

        let output_file_path = out_path.join("wdf.rs");
        Ok(bindgen_builder
            .generate()
            .expect("Bindings should succeed to generate")
            .write_to_file(&output_file_path)
            .map_err(|source| IoError::with_path(output_file_path, source))?)
    } else {
        info!(
            "Skipping wdf.rs generation since driver_config is {:#?}",
            config.driver_config
        );
        Ok(())
    }
}

#[cfg(feature = "gpio")]
fn generate_gpio(out_path: &Path, config: &Config) -> Result<(), ConfigError> {
    info!("Generating bindings to WDK: gpio.rs");

    let header_contents =
        config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf, ApiSubset::Gpio])?;
    trace!(header_contents = ?header_contents);

    let bindgen_builder = {
        let mut builder = bindgen::Builder::wdk_default(config)?
            .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement())
            .header_contents("gpio-input.h", &header_contents);

        // Only allowlist files in the gpio-specific files to avoid
        // duplicate definitions
        for header_file in config.headers(ApiSubset::Gpio)? {
            builder = builder.allowlist_file(format!("(?i).*{header_file}.*"));
        }
        builder
    };
    trace!(bindgen_builder = ?bindgen_builder);

    let output_file_path = out_path.join("gpio.rs");
    Ok(bindgen_builder
        .generate()
        .expect("Bindings should succeed to generate")
        .write_to_file(&output_file_path)
        .map_err(|source| IoError::with_path(output_file_path, source))?)
}

#[cfg(feature = "iddcx")]
fn generate_iddcx(out_path: &Path, config: &Config) -> Result<(), ConfigError> {
    info!("Generating bindings to WDK: iddcx.rs");

    let header_contents =
        config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf, ApiSubset::Iddcx])?;
    trace!(header_contents = ?header_contents);

    let bindgen_builder = {
        bindgen::Builder::wdk_default(config)?
            // IddCx.h MUST be parsed as C++: under wdk-sys's default C mode the IDARG_* typedef names
            // hit clang's "must use 'struct' tag" (verified: IddCx.h parses with 0 errors as C++, fails
            // as C). `IDD_STUB` puts IddCxFuncEnum.h in table-dispatch mode (else its `#error
            // IDDCX_VERSION_MAJOR is not defined` fires — it's inside `#ifndef IDD_STUB`). Matches the
            // proven wdf-umdf oracle's `--language=c++` + `#define IDD_STUB`. NOT WDF_STUB (wdk-sys
            // parses wdf.h non-stubbed; stubbing only here would desync WDF type-identity).
            .clang_arg("--language=c++")
            .clang_arg("-DIDD_STUB")
            // Parsing as C++ makes bindgen preserve the `UINT` typedef as the underlying repr of the
            // DXGI/OPM enums it emits as `pub mod _X { pub type Type = UINT; }` — and `UINT` isn't in
            // scope inside those nested modules. Translate enum repr types to native Rust ints (u32) so
            // the enum modules are self-contained. (Struct fields still use `UINT`, covered by the
            // alias in src/iddcx.rs.)
            .translate_enum_integer_types(true)
            // Emit ONLY IddCx items (its types + the IddFunctions table enums + the DDI fn-pointer
            // typedefs) and DON'T recurse into referenced WDF/Win/DXGI types — those resolve to wdk-sys's
            // shared bindings via `use crate::types::*` in src/iddcx.rs (type-identity, no re-emission,
            // no fragile blocklist). Full codegen (no .complement()) so the IDARG_* structs are emitted.
            .allowlist_recursively(false)
            .header_contents("iddcx-input.h", &header_contents)
            .allowlist_file("(?i).*iddcx.*")
            // IddCx structs reference DXGI + OPM (output-protection) types and the `UINT` primitive that
            // wdk-sys does NOT bindgen. Emit those locally (non-conflicting — they're absent from
            // crate::types). WDF/GUID/ULONG types it shares DO resolve from crate::types (no WDF type is
            // missing), so this stays type-identical for the WDF surface.
            .allowlist_type("_?DXGI_.*")
            .allowlist_type("IDXGI.*")
            // OPM types are `typedef struct _OPM_X {..} OPM_X` — allowlist BOTH the tag and the typedef
            // (allowlist_recursively(false) won't pull the tag in from the typedef). Plus D3DCOLORVALUE,
            // referenced by an OPM struct.
            .allowlist_type("_?OPM_.*")
            .allowlist_type("_?D3DCOLORVALUE")
    };
    trace!(bindgen_builder = ?bindgen_builder);

    let output_file_path = out_path.join("iddcx.rs");
    Ok(bindgen_builder
        .generate()
        .expect("Bindings should succeed to generate")
        .write_to_file(&output_file_path)
        .map_err(|source| IoError::with_path(output_file_path, source))?)
}

#[cfg(feature = "hid")]
fn generate_hid(out_path: &Path, config: &Config) -> Result<(), ConfigError> {
    info!("Generating bindings to WDK: hid.rs");

    let header_contents =
        config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf, ApiSubset::Hid])?;
    trace!(header_contents = ?header_contents);

    let bindgen_builder = {
        let mut builder = bindgen::Builder::wdk_default(config)?
            .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement())
            .header_contents("hid-input.h", &header_contents);

        // Only allowlist files in the hid-specific files to avoid
        // duplicate definitions
        for header_file in config.headers(ApiSubset::Hid)? {
            builder = builder.allowlist_file(format!("(?i).*{header_file}.*"));
        }
        builder
    };
    trace!(bindgen_builder = ?bindgen_builder);

    let output_file_path = out_path.join("hid.rs");
    Ok(bindgen_builder
        .generate()
        .expect("Bindings should succeed to generate")
        .write_to_file(&output_file_path)
        .map_err(|source| IoError::with_path(output_file_path, source))?)
}

#[cfg(feature = "parallel-ports")]
fn generate_parallel_ports(out_path: &Path, config: &Config) -> Result<(), ConfigError> {
    info!("Generating bindings to WDK: parallel_ports.rs");

    let header_contents = config.bindgen_header_contents([
        ApiSubset::Base,
        ApiSubset::Wdf,
        ApiSubset::ParallelPorts,
    ])?;
    trace!(header_contents = ?header_contents);

    let bindgen_builder = {
        let mut builder = bindgen::Builder::wdk_default(config)?
            .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement())
            .header_contents("parallel-ports-input.h", &header_contents);

        // Only allowlist files in the parallel-ports-specific files to
        // avoid duplicate definitions
        for header_file in config.headers(ApiSubset::ParallelPorts)? {
            builder = builder.allowlist_file(format!("(?i).*{header_file}.*"));
        }
        builder
    };
    trace!(bindgen_builder = ?bindgen_builder);

    let output_file_path = out_path.join("parallel_ports.rs");
    Ok(bindgen_builder
        .generate()
        .expect("Bindings should succeed to generate")
        .write_to_file(&output_file_path)
        .map_err(|source| IoError::with_path(output_file_path, source))?)
}

#[cfg(feature = "spb")]
fn generate_spb(out_path: &Path, config: &Config) -> Result<(), ConfigError> {
    info!("Generating bindings to WDK: spb.rs");

    let header_contents =
        config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf, ApiSubset::Spb])?;
    trace!(header_contents = ?header_contents);

    let bindgen_builder = {
        let mut builder = bindgen::Builder::wdk_default(config)?
            .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement())
            .header_contents("spb-input.h", &header_contents);

        // Only allowlist files in the spb-specific files to avoid
        // duplicate definitions
        for header_file in config.headers(ApiSubset::Spb)? {
            builder = builder.allowlist_file(format!("(?i).*{header_file}.*"));
        }
        builder
    };
    trace!(bindgen_builder = ?bindgen_builder);

    let output_file_path = out_path.join("spb.rs");
    Ok(bindgen_builder
        .generate()
        .expect("Bindings should succeed to generate")
        .write_to_file(&output_file_path)
        .map_err(|source| IoError::with_path(output_file_path, source))?)
}

#[cfg(feature = "storage")]
fn generate_storage(out_path: &Path, config: &Config) -> Result<(), ConfigError> {
    info!("Generating bindings to WDK: storage.rs");

    let header_contents =
        config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf, ApiSubset::Storage])?;
    trace!(header_contents = ?header_contents);

    let bindgen_builder = {
        let mut builder = bindgen::Builder::wdk_default(config)?
            .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement())
            .header_contents("storage-input.h", &header_contents);

        // Only allowlist files in the storage-specific files to avoid
        // duplicate definitions
        for header_file in config.headers(ApiSubset::Storage)? {
            builder = builder.allowlist_file(format!("(?i).*{header_file}.*"));
        }
        builder
    };
    trace!(bindgen_builder = ?bindgen_builder);

    let output_file_path = out_path.join("storage.rs");
    Ok(bindgen_builder
        .generate()
        .expect("Bindings should succeed to generate")
        .write_to_file(&output_file_path)
        .map_err(|source| IoError::with_path(output_file_path, source))?)
}

#[cfg(feature = "usb")]
fn generate_usb(out_path: &Path, config: &Config) -> Result<(), ConfigError> {
    info!("Generating bindings to WDK: usb.rs");

    let header_contents =
        config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf, ApiSubset::Usb])?;
    trace!(header_contents = ?header_contents);

    let bindgen_builder = {
        let mut builder = bindgen::Builder::wdk_default(config)?
            .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement())
            .header_contents("usb-input.h", &header_contents);

        // Only allowlist files in the usb-specific files to avoid
        // duplicate definitions
        for header_file in config.headers(ApiSubset::Usb)? {
            builder = builder.allowlist_file(format!("(?i).*{header_file}.*"));
        }
        builder
    };
    trace!(bindgen_builder = ?bindgen_builder);

    let output_file_path = out_path.join("usb.rs");
    Ok(bindgen_builder
        .generate()
        .expect("Bindings should succeed to generate")
        .write_to_file(&output_file_path)
        .map_err(|source| IoError::with_path(output_file_path, source))?)
}

/// Generates a `wdf_function_count.rs` file in `OUT_DIR` which contains the
/// definition of the function `get_wdf_function_count()`. This is required to
/// be generated here since the size of the table is derived from either a
/// global symbol that newer WDF versions expose, or an enum that older versions
/// use.
fn generate_wdf_function_count(out_path: &Path, config: &Config) -> Result<(), IoError> {
    const MINIMUM_MINOR_VERSION_TO_GENERATE_WDF_FUNCTION_COUNT: u8 = 25;

    let generated_file_path = out_path.join("wdf_function_count.rs");
    let mut generated_file = File::create(&generated_file_path)
        .map_err(|source| IoError::with_path(&generated_file_path, source))?;

    let is_wdf_function_count_generated = match *config {
        Config {
            driver_config:
                DriverConfig::Kmdf(KmdfConfig {
                    kmdf_version_major,
                    target_kmdf_version_minor,
                    ..
                }),
            ..
        } => {
            kmdf_version_major >= 1
                && target_kmdf_version_minor >= MINIMUM_MINOR_VERSION_TO_GENERATE_WDF_FUNCTION_COUNT
        }

        Config {
            driver_config:
                DriverConfig::Umdf(UmdfConfig {
                    umdf_version_major,
                    target_umdf_version_minor,
                    ..
                }),
            ..
        } => {
            umdf_version_major >= 2
                && target_umdf_version_minor >= MINIMUM_MINOR_VERSION_TO_GENERATE_WDF_FUNCTION_COUNT
        }

        _ => {
            unreachable!(
                "generate_wdf_function_table is only called with WDF driver configurations"
            )
        }
    };

    let wdf_function_table_count_snippet = WDF_FUNCTION_COUNT_FUNCTION_TEMPLATE.replace(
        WDF_FUNCTION_COUNT_PLACEHOLDER,
        if is_wdf_function_count_generated {
            WDF_FUNCTION_COUNT_DECLARATION_EXTERNAL_SYMBOL
        } else {
            WDF_FUNCTION_COUNT_DECLARATION_TABLE_INDEX
        },
    );

    generated_file
        .write_all(wdf_function_table_count_snippet.as_bytes())
        .map_err(|source| IoError::with_path(generated_file_path, source))?;
    Ok(())
}

/// Generates a `macros.rs` file in `OUT_DIR` which contains a
/// `call_unsafe_wdf_function_binding!` macro that redirects to the
/// `wdk_macros::call_unsafe_wdf_function_binding` `proc_macro` . This is
/// required in order to add an additional argument with the path to the file
/// containing generated types. There is currently no other way to pass
/// `OUT_DIR` of `wdk-sys` to the `proc_macro`.
fn generate_call_unsafe_wdf_function_binding_macro(out_path: &Path) -> Result<(), IoError> {
    let generated_file_path = out_path.join("call_unsafe_wdf_function_binding.rs");
    let mut generated_file = File::create(&generated_file_path)
        .map_err(|source| IoError::with_path(&generated_file_path, source))?;
    generated_file
        .write_all(
            CALL_UNSAFE_WDF_BINDING_TEMPLATE
                .replace(
                    OUT_DIR_PLACEHOLDER,
                    out_path.join("types.rs").to_str().expect(
                        "path to file with generated type information should successfully convert \
                         to a str",
                    ),
                )
                .as_bytes(),
        )
        .map_err(|source| IoError::with_path(generated_file_path, source))?;
    Ok(())
}

/// Generates a `test_stubs.rs` file in `OUT_DIR` which contains stubs required
/// for tests to compile. This should only generate the stubs whose names are
/// dependent on the WDK configuration, and would otherwise be impossible to
/// just include in `src/test_stubs.rs` directly.
fn generate_test_stubs(out_path: &Path, config: &Config) -> Result<(), IoError> {
    let stubs_file_path = out_path.join("test_stubs.rs");
    let mut stubs_file = File::create(&stubs_file_path)
        .map_err(|source| IoError::with_path(&stubs_file_path, source))?;
    stubs_file
        .write_all(
            TEST_STUBS_TEMPLATE
                .replace(
                    WDFFUNCTIONS_SYMBOL_NAME_PLACEHOLDER,
                    &config.compute_wdffunctions_symbol_name().expect(
                        "KMDF and UMDF configs should always have a computable WdfFunctions \
                         symbol name",
                    ),
                )
                .as_bytes(),
        )
        .map_err(|source| IoError::with_path(stubs_file_path, source))?;
    Ok(())
}

/// Starts parallel bindgen tasks for generating binding files.
fn start_bindgen_tasks<'scope>(
    thread_scope: &'scope thread::Scope<'scope, '_>,
    out_path: &'scope Path,
    config: &'scope Config,
    thread_join_handles: &mut Vec<thread::ScopedJoinHandle<'scope, Result<(), ConfigError>>>,
) {
    info_span!("bindgen generation").in_scope(|| {
        for (file_name, generate_function) in BINDGEN_FILE_GENERATORS_TUPLES {
            let current_span = Span::current();

            thread_join_handles.push(
                thread::Builder::new()
                    .name(format!("bindgen {file_name} generator"))
                    .spawn_scoped(thread_scope, move || {
                        // Parent span must be manually set since spans do not persist across thread boundaries: https://github.com/tokio-rs/tracing/issues/1391
                        info_span!(parent: &current_span, "worker thread", generated_file_name = file_name).in_scope(|| generate_function(out_path, config))
                    })
                    .expect("Scoped Thread should spawn successfully"),
            );
        }
    });
}

/// Starts a task that compiles a C shim to expose WDF symbols hidden by
/// `__declspec(selectany)`.
fn start_wdf_symbol_export_tasks<'scope>(
    thread_scope: &'scope thread::Scope<'scope, '_>,
    out_path: &'scope Path,
    config: &'scope Config,
    thread_join_handles: &mut Vec<thread::ScopedJoinHandle<'scope, Result<(), ConfigError>>>,
) {
    let current_span = Span::current();

    // Compile a c library to expose symbols that are not exposed because of
    // __declspec(selectany)
    thread_join_handles.push(
        thread::Builder::new()
            .name("wdf.c cc compilation".to_string())
            .spawn_scoped(thread_scope, move || {
                // Parent span must be manually set since spans do not persist across thread boundaries: https://github.com/tokio-rs/tracing/issues/1391
                info_span!(parent: current_span, "cc").in_scope(|| {
                    info!("Compiling wdf.c");

                    // Write all included headers into wdf.c (existing file, if present
                    // (i.e. incremental rebuild), is truncated)
                    let wdf_c_file_path = out_path.join("wdf.c");
                    {
                        let mut wdf_c_file = File::create(&wdf_c_file_path)
                            .map_err(|source| IoError::with_path(&wdf_c_file_path, source))?;
                        wdf_c_file
                            .write_all(
                                config
                                    // This should include the entirety of the `ENABLED_API_SUBSETS`, but this is currently blocked by issues with mutually exclusive headers: https://github.com/microsoft/windows-drivers-rs/issues/515
                                    .bindgen_header_contents([
                                        ApiSubset::Base,
                                        ApiSubset::Wdf,
                                        #[cfg(feature = "hid")]
                                        ApiSubset::Hid,
                                        #[cfg(feature = "spb")]
                                        ApiSubset::Spb,
                                    ])?
                                    .as_bytes(),
                            )
                            .map_err(|source| IoError::with_path(&wdf_c_file_path, source))?;

                        // Explicitly sync_all to surface any IO errors (File::drop
                        // silently ignores close errors)
                        wdf_c_file
                            .sync_all()
                            .map_err(|source| IoError::with_path(&wdf_c_file_path, source))?;
                    }

                    let mut cc_builder = cc::Build::new();
                    for (key, value) in config.preprocessor_definitions() {
                        cc_builder.define(&key, value.as_deref());
                    }

                    cc_builder
                        .includes(config.include_paths()?)
                        .file(wdf_c_file_path)
                        .compile("wdf");
                    Ok::<(), ConfigError>(())
                })
            })
            .expect("Scoped Thread should spawn successfully"),
    );
}

/// Starts generation/compilation tasks for WDF-specific artifacts for driver
/// configurations.
///
/// Uses the `start_*_tasks` naming convention: dispatches work to scoped
/// threads and returns after scheduling.
fn start_wdf_artifact_tasks<'scope>(
    thread_scope: &'scope thread::Scope<'scope, '_>,
    out_path: &'scope Path,
    config: &'scope Config,
    thread_join_handles: &mut Vec<thread::ScopedJoinHandle<'scope, Result<(), ConfigError>>>,
) -> anyhow::Result<()> {
    if let DriverConfig::Kmdf(_) | DriverConfig::Umdf(_) = config.driver_config {
        start_wdf_symbol_export_tasks(thread_scope, out_path, config, thread_join_handles);

        info_span!("wdf_function_count.rs generation")
            .in_scope(|| generate_wdf_function_count(out_path, config))?;

        info_span!("call_unsafe_wdf_function_binding.rs generation")
            .in_scope(|| generate_call_unsafe_wdf_function_binding_macro(out_path))?;

        info_span!("test_stubs.rs generation")
            .in_scope(|| generate_test_stubs(out_path, config))?;
    }
    Ok(())
}

/// Joins all worker threads and collects their results
fn join_worker_threads(
    thread_join_handles: Vec<thread::ScopedJoinHandle<'_, Result<(), ConfigError>>>,
) -> anyhow::Result<()> {
    for join_handle in thread_join_handles {
        let thread_name = join_handle.thread().name().unwrap_or("UNNAMED").to_string();

        match join_handle.join() {
            // Forward panics to the main thread
            Err(panic_payload) => {
                panic::resume_unwind(panic_payload);
            }

            Ok(thread_result) => {
                thread_result.with_context(|| {
                    format!(r#""{thread_name}" thread failed to exit successfully"#)
                })?;
            }
        }
    }
    Ok(())
}

fn main() -> anyhow::Result<()> {
    initialize_tracing()?;

    configure_wdk_library_build_and_then(|config| {
        let out_path = PathBuf::from(
            env::var("OUT_DIR").expect("OUT_DIR should be exist in Cargo build environment"),
        );

        thread::scope(|thread_scope| {
            let mut thread_join_handles = Vec::new();

            start_bindgen_tasks(thread_scope, &out_path, &config, &mut thread_join_handles);
            start_wdf_artifact_tasks(thread_scope, &out_path, &config, &mut thread_join_handles)?;

            join_worker_threads(thread_join_handles)
        })?;

        Ok::<(), anyhow::Error>(())
    })?;

    Ok(())
}
