//! A featherweight toolkit to help iterate over long and short commandline
//! arguments concisely. It's a comfy middleground between bloated libraries
//! like `clap` and painstakingly hacking something together yourself for each
//! new project.
//!
//! It doesn't handle taking positional arguments, but you can manually consume
//! later values in order of appearance. Do you really need bloated proc macros
//! when collecting arguments can be simplified to a `.next()`? You have zero
//! indication of what's going on under the hood, so you can't implement your
//! own behaviour.
//!
//! All the functionality boils down to `(&str).as_arg()`, which checks if the
//! string begins with `-` or `--`, and returns an appropriate iterator for
//! either a single long argument or each grouped short argument.  
//! Putting it in practice, the most basic code looks like this:
//!
//! ```rust,ignore
//! let mut argv = std::env::args();
//! // Skip executable
//! argv.next();
//! // For each separate argument
//! while let Some(args) = argv.next() {
//!     // For the single long argument or each combined short argument
//!     for arg in args.as_arg() {
//!         match arg {
//!             Argument::Long("help") | Argument::Short("h") => {
//!                 eprintln!("{HELP_TEXT}");
//!             },
//!             Argument::Long("value") | Argument::Short("v") => {
//!                 let value: isize = argv.next()?.parse()?;
//!                 do_something(value);
//!             },
//!             unknown => {
//!                 eprintln!("Unknown argument {unknown}");
//!             }
//!         }
//!     }
//! }
//! ```
//!
//! Note that in this case it is best to use `while let Some(var) = iter.next()`
//! over `for var in iter`. This is because `for` actually takes ownership of
//! the iterator, so you can't go to the next argument in the match body to grab
//! a positional value with a simple `iter.next().unwrap()`.
//!
//! ---
//!
//! That's still a lot of boilerplate. To make it a bit narrower, [`Argument`]s
//! can be constructed with the [`arg`] macro:
//!
//! ```rust,ignore
//! assert_eq!(Argument::Long("long"), arg!(--long));
//! assert_eq!(Argument::Short("s"), arg!(-s));
//! assert_eq!(Argument::Bare("other"), arg!("other"));
//!
//! match arg {
//!     arg!(--help) | arg!(-h) => {
//!         ...
//!     },
//!     arg!(--a-long-argument) => {
//!         ...
//!     },
//!     _ => {},
//! }
//! ```
//!
//! Usually you match both one long and short argument at once, so you can also
//! combine exactly one of each in `arg!(--help | -h)` or `arg!(-h | --help)`.
//!
//! ---
//!
//! There's still a few layers of indentation that can be cut down on though,
//! since they would rarely change. You can replace the `for` and `match` with a
//! single [`match_arg`]:
//!
//! ```rust,ignore
//! let mut argv = std::env::args();
//! // Skip executable
//! argv.next();
//! while let Some(args) = argv.next() {
//!     match_arg!(args; {
//!         arg!(-h | --help) => {
//!             ...
//!         },
//!         _ => {},
//!     });
//! }
//! ```
//!
//! Or if you don't need any control between the `while` and `for`, you can cut
//! right to the meat of the logic and opt for [`for_args`]:
//!
//! ```rust,ignore
//! let mut argv = std::env::args();
//! // Skip executable
//! argv.next();
//! for_args!(argv; {
//!     arg!(-h | --help) => {
//!         ...
//!     },
//!     _ => {},
//! });
//! ```
//!
//! Note that if you need to match (secret?) arguments with spaces, funky
//! characters Rust doesn't recognize as part of an identifier (excluding dashes
//! which there's an edge case for), or if you want to match arbitrary long,
//! short, or bare arguments separately, you'll need to manually construct
//! `Argument::Long(var)`, `Argument::Short(var)`, or `Argument::Bare(var)`.
//!
//! If you're wondering why the `Short` variant carries a `&str` rather than a
//! `char`, it's to make it possible to bind them to the same `match` variable,
//! and also because macros in [`arg`] can't convert `ident`s into a `char`.  
//! If you accidentally try matching `arg!(-abc)`, _it will silently fail,_ and
//! there can't be any checks for it. Blame Rust I guess.


use std::{ iter, fmt, };


/// Quick way to contruct [`Argument`]s. Supports long and short arguments by
/// prepending one or two dashes, or "bare" arguments that aren't classified as
/// either long or short.
///
/// Long arguments support anything Rust considers an "identifier" (variable
/// name), separated by at most one dash. For example `arg!(--long)` or
/// `arg!(--long-arg)` are valid, but `arg!(--long--arg)` isn't.
///
/// Short arguments must only be one character long (eg. `arg!(-a)`). This
/// requirement _isn't checked_ due to limitations of `macro_rules`, and I'm not
/// writing an entire proc macro crate for one shortcut.  
/// If you use more than one character, it won't ever be matched.
///
/// In a match block, you can "or" exactly one long and short argument like
/// `arg!(-h | --help)`, which is equivalent to `arg!(-h) | arg!(--help)`.
///
/// Bare arguments (anything not valid as a long or short) must be quoted, for
/// example `arg!("bare-argument")`. Putting one or two dashes in front of the
/// argument will never match as it'd be recognized as a valid argument, but
/// three would be parsed as a bare (eg. `arg!("---lol")`).
///
/// The `arg!(+...)` syntax is used internally to combine Rust identifiers
/// separated by dashes.
#[macro_export]
macro_rules! arg {
    // Long and short arguments together
    (-- $($long:ident)-+ | - $short:ident) => {
        arg!(-- $($long)-+) | arg!(- $short)
    };
    (- $short:ident | -- $($long:ident)-+) => {
         arg!(- $short) | arg!(-- $($long)-+)
    };

    // Long argument
    (-- $($a:ident)-+) => {
        $crate::Argument::Long(arg!(+ $($a)-+))
    };

    // Short argument
    (- $a:ident) => {
        $crate::Argument::Short(stringify!($a))
    };

    // Anything else to match to
    ($a:literal) => {
        $crate::Argument::Bare($a)
    };

    // If long argument contains dashes, combine them using `arg!(+...)`
    (+ $first:ident) => {
        stringify!($first)
    };
    (+ $first:ident - $($next:ident)-+) => {
        concat!(stringify!($first), "-", arg!(+ $($next)-+))
    };
}

/// Matches each part of a single argument - once if it's a long argument (eg.
/// `--help`), or for each character of combined short arguments (eg. `-abc`).
///
/// Expands to:
///
/// ```rust,ignore
/// for arg in $str.as_arg() {
///     match arg { $match }
/// }S
/// ```
#[macro_export]
macro_rules! match_arg {
    ($str:expr; $match:tt) => {
        for __ak_arg in $str.as_arg() {
            match __ak_arg $match
        }
    }
}

/// Matches each part of each argument in a `&str` iterator (like `env::args`),
/// using the [`match_arg`] macro.
///
/// Expands to:
///
/// ```rust,ignore
/// while let Some(args) in $iter.next() {
///     for arg in args.as_arg() {
///         match arg { $match }
///     }
/// }
/// ```
#[macro_export]
macro_rules! for_args {
    ($iter:expr; $match:tt) => {
        while let Some(__ak_args) = $iter.next() {
            $crate::match_arg!(__ak_args; $match)
        }
    }
}


/// A single argument that can be matched from an [`ArgumentIterator`].
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum Argument<'a> {
    /// A long argument without the leading dashes.  
    /// `Argument::Long("help") == arg!(--help)`
    Long(&'a str),
    /// A short argument without the leading dash.  
    /// `Argument::Short("h") == arg!(-h)`
    ///
    /// Important to note that it must be only 1 character long or it
    /// will never match, but for ergonimic reasons it is actually a `&str`.
    Short(&'a str),
    /// Raw string of anything else passed as an argument, whether it has zero
    /// or three dashes.  
    /// `Argument::Bare("---yeet") == arg!("---yeet")`
    ///
    /// Exists as an alternative to putting parsed arguments in a `Result`.
    Bare(&'a str),
}

impl fmt::Display for Argument<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Long(long) => write!(f, "--{long}"),
            Self::Short(short) => write!(f, "-{short}"),
            Self::Bare(bare) => write!(f, "{bare}"),
        }
    }
}

impl Argument<'_> {
    pub fn is_long(&self) -> bool {
        matches!(self, Self::Long(_))
    }

    pub fn is_short(&self) -> bool {
        matches!(self, Self::Short(_))
    }

    pub fn is_bare(&self) -> bool {
        matches!(self, Self::Bare(_))
    }
}


/// An iterator over a string's arguments - equivalent to an `iter::once(&str)`
/// if the argument is long or bare, or `str::chars()` for each combined short
/// argument (except returning `&str`s for ergonomic reasons).
///
/// Constructed with `(&str).as_arg()` (see [`AsArgumentIterator`]).
#[derive(Clone)]
pub struct ArgumentIterator<'a>(ArgumentIteratorState<'a>);

/// Private state of an iterator. Implementation is horrible, please don't look
/// at it.
#[derive(Clone)]
enum ArgumentIteratorState<'a> {
    Long(Option<&'a str>),
    Short(&'a str),
    Bare(Option<&'a str>),
}

impl<'a> iter::Iterator for ArgumentIterator<'a> {
    type Item = Argument<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        match self.0 {
            ArgumentIteratorState::Long(ref mut long) => Some(Argument::Long(long.take()?)),
            ArgumentIteratorState::Short(ref mut short) => {
                let (one, rest) = short.split_at_checked(1)?;
                *short = rest;
                Some(Argument::Short(one))
            },
            ArgumentIteratorState::Bare(ref mut bare) => Some(Argument::Bare(bare.take()?)),
        }
    }
}


pub trait AsArgumentIterator {
    fn as_arg<'a>(&'a self) -> ArgumentIterator<'a>;
}

impl AsArgumentIterator for str {
    fn as_arg<'a>(&'a self) -> ArgumentIterator<'a> {
        if self.starts_with("---") {
            return ArgumentIterator(ArgumentIteratorState::Bare(Some(self)));
        }

        if let Some(long) = self.strip_prefix("--") {
            if long.is_empty() {
                ArgumentIterator(ArgumentIteratorState::Bare(Some(self)))
            } else {
                ArgumentIterator(ArgumentIteratorState::Long(Some(long)))
            }
        } else if let Some(short) = self.strip_prefix("-") {
            if short.is_empty() {
                ArgumentIterator(ArgumentIteratorState::Bare(Some(self)))
            } else {
                ArgumentIterator(ArgumentIteratorState::Short(short))
            }
        } else {
            ArgumentIterator(ArgumentIteratorState::Bare(Some(self)))
        }
    }
}


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn long() {
        let long_arg = "--help";
        let mut long_iter = long_arg.as_arg();
        assert_eq!(long_iter.next(), Some(arg!(--help)));
        assert_eq!(long_iter.next(), None);
    }

    #[test]
    fn short() {
        let short_arg = "-abc";
        let mut short_iter = short_arg.as_arg();
        assert_eq!(short_iter.next(), Some(arg!(-a)));
        assert_eq!(short_iter.next(), Some(arg!(-b)));
        assert_eq!(short_iter.next(), Some(arg!(-c)));
        assert_eq!(short_iter.next(), None);
    }

    #[test]
    fn long_no_ident() {
        let long_arg = "--";
        let mut long_iter = long_arg.as_arg();
        assert_eq!(long_iter.next(), Some(arg!("--")));
        assert_eq!(long_iter.next(), None);
    }

    #[test]
    fn short_no_ident() {
        let short_arg = "-";
        let mut short_iter = short_arg.as_arg();
        assert_eq!(short_iter.next(), Some(arg!("-")));
        assert_eq!(short_iter.next(), None);
    }

    #[test]
    fn long_with_dash() {
        let long_arg = "--abc-def";
        let mut long_iter = long_arg.as_arg();
        assert_eq!(long_iter.next(), Some(arg!(--abc-def)));
        assert_eq!(long_iter.next(), None);
    }

    #[test]
    fn no_dashes() {
        let other_arg = "yeet";
        let mut other_iter = other_arg.as_arg();
        assert_eq!(other_iter.next(), Some(arg!("yeet")));
        assert_eq!(other_iter.next(), None);
    }

    #[test]
    fn three_dashes() {
        let other_arg = "---yeet";
        let mut other_iter = other_arg.as_arg();
        assert_eq!(other_iter.next(), Some(arg!("---yeet")));
        assert_eq!(other_iter.next(), None);
    }

    #[test]
    fn match_long() {
        let mut state = false;
        match_arg!("--hello"; {
            arg!(--hello) => {
                assert_eq!(state, false);
                state = true;
            },
            _ => panic!(),
        });
    }

    #[test]
    fn match_short() {
        let mut state = 0;
        match_arg!("-abc"; {
            arg!(-a) => {
                assert_eq!(state, 0);
                state = 1;
            },
            arg!(-b) => {
                assert_eq!(state, 1);
                state = 2;
            },
            arg!(-c) => {
                assert_eq!(state, 2);
                state = 3;
            },
            _ => panic!(),
        });
    }

    #[test]
    fn for_long_short_bare() {
        let mut args = ["-a", "not_an_arg", "--blah", "-cd"].into_iter();
        let mut state = 0;
        for_args!(args; {
            arg!(-a | --blah) => {
                state = match state {
                    0 => 1,
                    2 => 3,
                    _ => panic!(),
                }
            },
            arg!("not_an_arg") | arg!(-d) => {
                state = match state {
                    1 => 2,
                    4 => 5,
                    _ => panic!(),
                }
            },
            arg!(-c) => {
                assert_eq!(state, 3);
                state = 4;
            },
            _ => panic!(),
        });
    }
}
