use std::ffi::OsString;
use std::path::PathBuf;
use brodo::slice::Axis;
use crate::error::MestoloError;
pub const DEFAULT_LIMIT: u64 = 4 << 30;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Input {
pub path: PathBuf,
pub limit: u64,
}
impl Input {
pub fn new(path: PathBuf) -> Self {
Self { path, limit: DEFAULT_LIMIT }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Output {
pub path: PathBuf,
pub force: bool,
}
impl Output {
pub fn new(path: PathBuf, force: bool) -> Self {
Self { path, force }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Format {
Vdb,
Vtu,
Cache,
Archive,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Command {
Help,
Info(Input),
Slice {
input: Input,
grid: Option<String>,
axis: Axis,
at: i32,
output: Output,
},
Convert {
input: Input,
grid: Option<String>,
format: Option<Format>,
output: Output,
},
}
#[derive(Default)]
struct Given {
input: Option<PathBuf>,
grid: Option<String>,
axis: Option<Axis>,
at: Option<i32>,
output: Option<PathBuf>,
force: bool,
limit: Option<u64>,
format: Option<Format>,
}
pub fn parse(args: impl IntoIterator<Item = OsString>) -> Result<Command, MestoloError> {
let mut args = args.into_iter();
let Some(name) = args.next() else {
return Err(MestoloError::NoCommand);
};
let name = text(name, "the command")?;
let takes: &[&str] = match name.as_str() {
"help" | "-h" | "--help" => return Ok(Command::Help),
"info" => &["--limit"],
"slice" => &["--limit", "--grid", "--axis", "--at", "-o", "--force"],
"convert" => &["--limit", "--grid", "--as", "-o", "--force"],
_ => return Err(MestoloError::UnknownCommand(name)),
};
let given = options(args, takes)?;
let input = Input {
path: given.input.ok_or(MestoloError::Missing("the input file"))?,
limit: given.limit.unwrap_or(DEFAULT_LIMIT),
};
let output = given.output.map(|path| Output { path, force: given.force });
if name == "info" {
return Ok(Command::Info(input));
}
let output = output.ok_or(MestoloError::Missing("-o"))?;
if name == "convert" {
return Ok(Command::Convert { input, grid: given.grid, format: given.format, output });
}
Ok(Command::Slice {
input,
grid: given.grid,
axis: given.axis.ok_or(MestoloError::Missing("--axis"))?,
at: given.at.ok_or(MestoloError::Missing("--at"))?,
output,
})
}
fn options(
mut args: impl Iterator<Item = OsString>,
takes: &[&str],
) -> Result<Given, MestoloError> {
let mut given = Given::default();
let mut ended = false;
while let Some(arg) = args.next() {
let flag = arg.to_str().filter(|a| !ended && a.starts_with('-') && *a != "-");
let Some(flag) = flag.map(str::to_owned) else {
if given.input.is_some() {
return Err(MestoloError::Extra(arg.to_string_lossy().into_owned()));
}
given.input = Some(PathBuf::from(arg));
continue;
};
if flag == "--" {
ended = true;
continue;
}
if !takes.contains(&flag.as_str()) {
return Err(MestoloError::UnknownOption(flag));
}
set(&mut given, &flag, &mut args)?;
}
Ok(given)
}
fn set(
given: &mut Given,
flag: &str,
args: &mut impl Iterator<Item = OsString>,
) -> Result<(), MestoloError> {
let twice = || MestoloError::Repeated(flag.to_owned());
match flag {
"--force" if given.force => return Err(twice()),
"--force" => given.force = true,
"-o" => once(&mut given.output, PathBuf::from(value(args, "-o")?), twice)?,
"--grid" => once(&mut given.grid, text(value(args, "--grid")?, "--grid")?, twice)?,
"--axis" => once(&mut given.axis, axis(&text(value(args, "--axis")?, "--axis")?)?, twice)?,
"--at" => once(&mut given.at, number(value(args, "--at")?, "--at")?, twice)?,
"--as" => once(&mut given.format, format(&text(value(args, "--as")?, "--as")?)?, twice)?,
_ => once(&mut given.limit, number(value(args, "--limit")?, "--limit")?, twice)?,
}
Ok(())
}
fn once<T>(
slot: &mut Option<T>,
v: T,
twice: impl Fn() -> MestoloError,
) -> Result<(), MestoloError> {
if slot.replace(v).is_some() { Err(twice()) } else { Ok(()) }
}
fn value(
args: &mut impl Iterator<Item = OsString>,
option: &'static str,
) -> Result<OsString, MestoloError> {
args.next().ok_or(MestoloError::Missing(option))
}
fn text(arg: OsString, what: &'static str) -> Result<String, MestoloError> {
arg.into_string().map_err(|_| MestoloError::NotText(what))
}
fn number<T: std::str::FromStr>(arg: OsString, option: &'static str) -> Result<T, MestoloError> {
let s = text(arg, option)?;
s.parse().map_err(|_| MestoloError::BadValue { option, value: s })
}
fn format(s: &str) -> Result<Format, MestoloError> {
match s {
"vdb" => Ok(Format::Vdb),
"vtu" => Ok(Format::Vtu),
"cache" => Ok(Format::Cache),
"archive" => Ok(Format::Archive),
_ => Err(MestoloError::BadValue { option: "--as", value: s.to_owned() }),
}
}
fn axis(s: &str) -> Result<Axis, MestoloError> {
match s {
"x" => Ok(Axis::X),
"y" => Ok(Axis::Y),
"z" => Ok(Axis::Z),
_ => Err(MestoloError::BadValue { option: "--axis", value: s.to_owned() }),
}
}