use std::{
	env,
	path::PathBuf,
};

use bindgen::callbacks::{
	EnumVariantValue,
	ParseCallbacks,
};

const ENUMS: &[(&str, &str)] = &[
	("TidyAttrId", "TidyAttr_"),
	("TidyAttrSortStrategy", "TidySortAttr"),
	("TidyConfigCategory", "Tidy"),
	("TidyDoctypeModes", "TidyDoctype"),
	("TidyDupAttrModes", "Tidy"),
	("TidyEncodingOptions", "TidyEnc"),
	("TidyFormatParameterType", "tidyFormatType_"),
	("TidyLineEnding", "Tidy"),
	("TidyNodeType", "TidyNode_"),
	("TidyOptionId", "Tidy"),
	("TidyOptionType", "Tidy"),
	("TidyReportLevel", "Tidy"),
	("tidyStrings", "-"),
	("TidyTagId", "TidyTag_"),
	("TidyTriState", "Tidy"),
	("TidyUppercase", "TidyUppercase"),
	("TidyUseCustomTagsState", "TidyCustom"),
];

const HEADER: &str = r#"
#include "tidy.h"
#include "tidybuffio.h"
#include "tidyplatform.h"
#include "tidyenum.h"
"#;

const fn onoff(on: bool) -> &'static str {
	if on {
		"ON"
	} else {
		"OFF"
	}
}

#[derive(Debug)]
struct ParseCallback;

impl ParseCallbacks for ParseCallback {
	fn enum_variant_name(
		&self,
		enum_name: Option<&str>,
		variant: &str,
		_val: EnumVariantValue,
	) -> Option<String> {
		let enum_name = enum_name?;
		for &(name, prefix) in ENUMS {
			if name == enum_name {
				return variant.strip_prefix(prefix).map(str::to_string);
			}
		}

		None
	}
}

fn is_feature(s: &str) -> bool {
	env::var(format!("CARGO_FEATURE_{}", s.to_uppercase())).is_ok()
}

fn main() {
	let cmake_defines = &[
		("ENABLE_MEMORY_DEBUG", "off"),
		("ENABLE_DEBUG_LOG", "off"),
		("ENABLE_CRTDBG_MEMORY", "off"),
		("ENABLE_ALLOC_DEBUG", "off"),
		("BUILD_SHARED_LIB", "off"),
		("SUPPORT_CONSOLE_APP", "off"),
		("SUPPORT_LOCALIZATIONS", onoff(is_feature("localization"))),
	];
	let profile = env::var("PROFILE").unwrap();
	let opt_level = env::var("OPT_LEVEL")
		.ok()
		.filter(|s| s.len() == 1 && "0123szg".contains(s))
		.unwrap_or_else(|| {
			String::from(match profile.as_str() {
				"release" | "bench" => "2",
				"debug" => "0",
				_ => {
					eprintln!("warning: unrecognized cargo profile {profile}; using opt-level 2");
					"2"
				}
			})
		});
	let opt_flag = format!("-O{opt_level}");

	println!("cargo:rerun-if-changed=vendor/");

	for env in ["CMAKE_GENERATOR", "CC", "TIDY_SYS_CFLAGS", "CFLAGS"] {
		println!("cargo:rerun-if-env-changed={env}");
	}

	let mut cmake = cmake::Config::new("vendor/tidy.5.8.0");
	cmake.configure_arg("-DCMAKE_POLICY_VERSION_MINIMUM=3.5");
	for (k, v) in cmake_defines {
		cmake.define(k, v);
	}

	cmake
		.profile("Release") // Not setting this causes problems with msbuild
			.cflag(opt_flag)
			.cflag("-DNDEBUG");

	if let Ok(flags) = env::var("TIDY_SYS_CFLAGS") {
		eprintln!("info: using CFLAGS from the TIDY_SYS_CFLAGS environment variable with the value: {flags}");
		for s in flags.split_whitespace() {
			cmake.cflag(s);
		}
	}

	let p = cmake.build();

	println!("cargo:rustc-link-search=native={}/lib", p.display());
	if env::var_os("CARGO_CFG_WINDOWS").is_some() {
		println!("cargo:rustc-link-lib=static=tidy_static");
	} else {
		println!("cargo:rustc-link-lib=static=tidy");
	}

	// Generate bindings
	let mut builder = bindgen::Builder::default()
		.clang_arg(format!("-I{}/include", p.display()))
		.header_contents("wrapper.h", HEADER)
		.no_default("_?TidyDoc")
		.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
		.parse_callbacks(Box::new(ParseCallback))
		.allowlist_file(r".*[/\\]tidy.*")
		.enable_function_attribute_detection();

	for (name, _) in ENUMS {
		builder = builder.rustified_enum(*name);
	}

	let bindings = builder.generate().expect("failed to generate bindings");

	let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
	bindings
		.write_to_file(out_path.join("bindings.rs"))
		.unwrap();
}
