//! A deliberately tiny argument parser.
//!
//! Four subcommands do not justify a `clap` dependency and its build time. The
//! only subtlety is that some long options take a value (`--pid 165925`) and
//! some are bare booleans (`--utf16`). A parser cannot tell those apart from
//! the token stream alone, so each subcommand declares which of its options
//! take a value and we look the name up in that list.

use std::collections::HashMap;

pub struct Args {
    opts: HashMap<String, Option<String>>,
    pub positional: Vec<String>,
}

#[derive(Debug)]
pub struct ArgError(pub String);

impl std::fmt::Display for ArgError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl Args {
    /// `value_flags` lists the long option names that consume the following
    /// token as their value. Everything else beginning with `--` is a boolean.
    /// `--name=value` is always accepted regardless of the list.
    pub fn parse<I: Iterator<Item = String>>(
        argv: I,
        value_flags: &[&str],
    ) -> Result<Args, ArgError> {
        let mut opts: HashMap<String, Option<String>> = HashMap::new();
        let mut positional = Vec::new();
        let mut it = argv.peekable();

        while let Some(tok) = it.next() {
            if let Some(rest) = tok.strip_prefix("--") {
                if rest.is_empty() {
                    // A bare `--` ends option parsing; the rest is positional.
                    positional.extend(it.by_ref());
                    break;
                }
                if let Some((name, value)) = rest.split_once('=') {
                    opts.insert(name.to_string(), Some(value.to_string()));
                } else if value_flags.contains(&rest) {
                    let value = it
                        .next()
                        .ok_or_else(|| ArgError(format!("--{rest} needs a value")))?;
                    opts.insert(rest.to_string(), Some(value));
                } else {
                    opts.insert(rest.to_string(), None);
                }
            } else {
                positional.push(tok);
            }
        }

        Ok(Args { opts, positional })
    }

    pub fn has(&self, name: &str) -> bool {
        self.opts.contains_key(name)
    }

    pub fn value(&self, name: &str) -> Option<&str> {
        self.opts.get(name).and_then(|v| v.as_deref())
    }

    pub fn parse_value<T: std::str::FromStr>(&self, name: &str) -> Result<Option<T>, ArgError> {
        match self.value(name) {
            None => Ok(None),
            Some(raw) => raw
                .parse::<T>()
                .map(Some)
                .map_err(|_| ArgError(format!("could not parse --{name} value {raw:?}"))),
        }
    }

    /// Reject typos instead of silently ignoring them. `futmem find --acii foo`
    /// should not quietly scan for nothing.
    pub fn reject_unknown(&self, known: &[&str]) -> Result<(), ArgError> {
        for name in self.opts.keys() {
            if !known.contains(&name.as_str()) {
                return Err(ArgError(format!("unknown option --{name}")));
            }
        }
        Ok(())
    }
}

/// Parse `0x1234`, `1234` (hex assumed when the `0x` prefix is present,
/// decimal otherwise) into a virtual address.
pub fn parse_addr(raw: &str) -> Result<u64, ArgError> {
    let cleaned = raw.replace('_', "");
    let parsed = match cleaned
        .strip_prefix("0x")
        .or_else(|| cleaned.strip_prefix("0X"))
    {
        Some(hex) => u64::from_str_radix(hex, 16),
        // Bare addresses in this project are always written in hex
        // (`6ffffc140000`), so try hex first and fall back to decimal only for
        // values that are unambiguous.
        None => u64::from_str_radix(&cleaned, 16).or_else(|_| cleaned.parse::<u64>()),
    };
    parsed.map_err(|_| ArgError(format!("bad address {raw:?}")))
}

/// Parse a length: `4096`, `0x1000`, `16k`, `2m`.
pub fn parse_len(raw: &str) -> Result<u64, ArgError> {
    let lower = raw.to_ascii_lowercase();
    let (body, mult) = match lower.strip_suffix('k') {
        Some(b) => (b, 1024u64),
        None => match lower.strip_suffix('m') {
            Some(b) => (b, 1024 * 1024),
            None => (lower.as_str(), 1),
        },
    };
    let n = match body.strip_prefix("0x") {
        Some(hex) => u64::from_str_radix(hex, 16),
        None => body.parse::<u64>(),
    }
    .map_err(|_| ArgError(format!("bad length {raw:?}")))?;
    Ok(n * mult)
}
