// Copyright (C) 2024-2025  Johannes Sasongko <johannes sasongko org>
// SPDX-License-Identifier: GPL-3.0-or-later

#![cfg(windows)]

use std::{
	ffi::{OsStr, OsString},
	path::{Path, PathBuf},
};

/// Searches for possible locations of a program.
///
/// This is needed to correctly launch a program on Windows because
/// [`Command`](std::process::Command) uses [`CreateProcessW`], which searches
/// weird places before `PATH`.
/// Instead, we just want to use `PATH`.
///
/// [`CreateProcessW`]: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw>.
pub fn get_possible_program_locations(
	program: impl AsRef<OsStr>,
	get_path_env: impl FnOnce() -> Option<OsString>,
) -> Vec<PathBuf> {
	let program = program.as_ref();
	if path_is_name_only(program) {
		get_path_env()
			.map(|path_env| {
				split_path_env(path_env)
					.into_iter()
					.map(|path| PathBuf::from_iter([&path, program]))
					.collect()
			})
			.unwrap_or_default()
	} else {
		vec![PathBuf::from(program)]
	}
}

/// Whether a path only consists of a [normal](std::path::Component::Normal)
/// (i.e. name) component, e.g. "cmd" or "cmd.exe" instead of "./cmd.exe".
fn path_is_name_only(exe: impl AsRef<Path>) -> bool {
	let mut components = Path::new(exe.as_ref()).components();
	if let Some(std::path::Component::Normal(_)) = components.next() {
		if components.next().is_none() {
			return true;
		}
	}
	false
}

/// Splits a Windows PATH-style value.
fn split_path_env(path_env: impl AsRef<OsStr>) -> Vec<OsString> {
	use std::os::windows::ffi::{OsStrExt as _, OsStringExt as _};
	path_env
		.as_ref()
		.encode_wide()
		.collect::<Vec<_>>()
		.split(|&c| c == ';' as _)
		.filter(|s| !s.is_empty())
		.map(OsString::from_wide)
		.collect()
}

#[cfg(test)]
mod tests {
	use std::ffi::{OsStr, OsString};

	#[test]
	fn test_get_possible_program_locations() {
		const TEST_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), r"\data\test");
		const PATH: &str = concat!(
			env!("CARGO_MANIFEST_DIR"),
			r"\data\test;",
			env!("WINDIR"),
			r"\System32"
		);
		let program_locs =
			super::get_possible_program_locations("cmd", || Some(OsString::from(PATH)));
		assert_eq!(
			program_locs,
			[
				OsString::from(format!(r"{TEST_DIR}\cmd")),
				OsString::from(format!(r"{windir}\System32\cmd", windir = env!("WINDIR"))),
			]
		);
	}

	#[test]
	fn test_path_is_name_only() {
		assert!(super::path_is_name_only(OsStr::new("Windows")));
		assert!(super::path_is_name_only(OsStr::new("1:Windows")));
		assert!(super::path_is_name_only(OsStr::new("CC:Windows")));
		assert!(super::path_is_name_only(OsStr::new("...")));

		assert!(!super::path_is_name_only(OsStr::new("C:\\Windows")));
		assert!(!super::path_is_name_only(OsStr::new("C:")));
		assert!(!super::path_is_name_only(OsStr::new("C:Windows")));
		assert!(!super::path_is_name_only(OsStr::new("")));
		assert!(!super::path_is_name_only(OsStr::new(".")));
		assert!(!super::path_is_name_only(OsStr::new("..")));
	}
}
