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

use std::ffi::OsString;

#[derive(Debug)]
pub enum ArgsResult {
	Options(Options),
	Help,
	Version,
	Err(String),
}

#[derive(Debug)]
pub struct Options {
	pub actions: Vec<Action>,
	pub directory: Option<OsString>,
	pub ignore_current_envs: bool,
	pub program: Option<OsString>,
	pub program_args: Vec<OsString>,
	pub zero_end: bool,
}

#[derive(Debug)]
pub enum Action {
	AddFile(OsString),
	AddVar(OsString, OsString),
	RemoveVar(OsString),
}

pub fn parse(args: impl IntoIterator<Item = OsString>) -> ArgsResult {
	let mut iter = args.into_iter();
	let mut actions = Vec::new();
	let mut program = None;
	let mut ignore_current_envs = false;
	let mut zero_end = false;
	let mut directory = None;
	while let Some(arg) = iter.next() {
		match arg.as_encoded_bytes() {
			b"--" => {
				program = iter.next();
				break;
			}
			b"--help" => return ArgsResult::Help,
			b"--version" => return ArgsResult::Version,
			b"-" | [b'-', b'-', ..] => {
				return ArgsResult::Err(format!("invalid option '{}'\n", arg.to_string_lossy()));
			}
			[b'-', rest @ ..] => {
				let mut rest = rest;
				let mut idx: usize = 1;
				loop {
					match rest {
						b"" => break,
						[b'f', rest1 @ ..] => {
							let value = if rest1.is_empty() {
								let Some(value) = iter.next() else {
									return ArgsResult::Err(
										"-f must be followed by a file path\n".to_owned(),
									);
								};
								value
							} else {
								plat::osstr_slice_from(&arg, idx + 1)
							};
							actions.push(Action::AddFile(value));
							break;
						}
						[b'u', rest1 @ ..] => {
							let value = if rest1.is_empty() {
								let Some(value) = iter.next() else {
									return ArgsResult::Err(
										"-u must be followed by a variable name\n".to_owned(),
									);
								};
								value
							} else {
								plat::osstr_slice_from(&arg, idx + 1)
							};
							actions.push(Action::RemoveVar(value));
							break;
						}
						[b'i', rest1 @ ..] => {
							ignore_current_envs = true;
							rest = rest1;
						}
						[b'0', rest1 @ ..] => {
							zero_end = true;
							rest = rest1;
						}
						[b'C', rest1 @ ..] => {
							let value = if rest1.is_empty() {
								let Some(value) = iter.next() else {
									return ArgsResult::Err(
										"-C must be followed by a directory\n".to_owned(),
									);
								};
								value
							} else {
								plat::osstr_slice_from(&arg, idx + 1)
							};
							directory = Some(value);
							break;
						}
						[b's', rest1 @ ..] => {
							let var = if rest1.is_empty() {
								iter.next()
							} else {
								Some(plat::osstr_slice_from(&arg, idx + 1))
							};
							let var = var.map(plat::parse_freestanding);
							let Some(Freestanding::Variable(name, value)) = var else {
								return ArgsResult::Err(
									"-s must be followed by a variable definition\n".to_owned(),
								);
							};
							actions.push(Action::AddVar(name, value));
							break;
						}
						_ => {
							return ArgsResult::Err(format!(
								"invalid option '-{}'\n",
								plat::osstr_slice_from(&arg, idx).to_string_lossy(),
							));
						}
					}
					idx += 1;
				}
			}
			_ => match plat::parse_freestanding(arg) {
				Freestanding::Variable(name, value) => actions.push(Action::AddVar(name, value)),
				Freestanding::Program(prog) => {
					program = Some(prog);
					break;
				}
			},
		}
	}
	if program.is_some() {
		if zero_end {
			return ArgsResult::Err("-0 and command must not be specified together\n".to_owned());
		}
	} else if directory.is_some() {
		return ArgsResult::Err("-C must be specified together with command\n".to_owned());
	}
	let program_args = iter.collect::<Vec<_>>();
	ArgsResult::Options(Options {
		actions,
		directory,
		ignore_current_envs,
		program,
		program_args,
		zero_end,
	})
}

enum Freestanding {
	Program(OsString),
	Variable(OsString, OsString),
}

#[cfg(windows)]
mod plat {
	use super::Freestanding;
	use std::{
		ffi::{OsStr, OsString},
		os::windows::ffi::{OsStrExt as _, OsStringExt as _},
	};

	pub fn osstr_slice_from(str: &OsStr, from: usize) -> OsString {
		OsString::from_wide(&str.encode_wide().skip(from).collect::<Vec<_>>())
	}

	pub fn parse_freestanding(str: impl AsRef<std::ffi::OsStr>) -> Freestanding {
		// This is inefficient, but it's the only non-unsafe way to do this while
		// OsStr::slice_encoded_bytes is unstable
		// https://github.com/rust-lang/rust/issues/118485
		let mut string = str.as_ref().encode_wide();
		let mut has_eq = false;
		let first = string
			.by_ref()
			.take_while(|&ch| {
				if ch == '=' as u16 {
					has_eq = true;
					false
				} else {
					true
				}
			})
			.collect::<Vec<_>>();
		let first = OsString::from_wide(&first);
		if has_eq {
			let second = string.collect::<Vec<_>>();
			Freestanding::Variable(first, OsString::from_wide(&second))
		} else {
			Freestanding::Program(first)
		}
	}
}

#[cfg(not(windows))]
mod plat {
	use super::Freestanding;
	use std::ffi::{OsStr, OsString};

	// See https://github.com/rust-lang/rust/blob/master/library/std/src/os/mod.rs
	// for the `target_os`es, and search `os/ffi` on the sidebar for modules
	// that may contain compatible `OsStr[ing]Ext` implementations.
	#[cfg(all(target_vendor = "fortanix", target_env = "sgx"))]
	use std::os::fortanix_sgx::ffi::{OsStrExt as _, OsStringExt as _};
	#[cfg(target_os = "hermit")]
	use std::os::hermit::ffi::{OsStrExt as _, OsStringExt as _};
	#[cfg(target_os = "solid_asp3")]
	use std::os::solid::ffi::{OsStrExt as _, OsStringExt as _};
	#[cfg(unix)]
	use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
	#[cfg(target_os = "wasi")]
	use std::os::wasi::ffi::{OsStrExt as _, OsStringExt as _};
	#[cfg(target_os = "xous")]
	use std::os::xous::ffi::{OsStrExt as _, OsStringExt as _};

	pub fn osstr_slice_from(str: &OsStr, from: usize) -> OsString {
		OsStr::from_bytes(&str.as_bytes()[from..]).to_owned()
	}

	pub fn parse_freestanding(string: impl Into<OsString>) -> Freestanding {
		let string = string.into();
		let bytes = string.as_bytes();
		// Waiting for slice::split_once
		// <https://github.com/rust-lang/rust/issues/112811>.
		match bytes.iter().position(|&b| b == b'=') {
			Some(pos) => Freestanding::Variable(
				OsString::from_vec(bytes[..pos].to_owned()),
				OsString::from_vec(bytes[(pos + 1)..].to_owned()),
			),
			None => Freestanding::Program(string),
		}
	}
}
