/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;

use shlex::split;

use crate::parser::ParseError;
use crate::utils::get_prefix_bounds;

/// Expands all aliases accounting for circular references and prefix matching.
///
/// * `lookup` - A function to expand a command name to another shell-like command.
///
/// * `args` - The original arguments to resolve aliases from.
/// The first argument should be the command name.
///
/// On success, returns a tuple of Vec<String>.  The first is the expanded arguments.  The second
/// is the in-order replacements that were made to get to the expanded arguments.  If the second
/// vector is empty, no replacements were made.  If the second vector has arguments, the 0th index
/// is what the user originally typed.
pub fn expand_aliases<S: ToString>(
    lookup: impl Fn(&str) -> Option<S>,
    args: &[impl ToString],
) -> Result<(Vec<String>, Vec<String>), ParseError> {
    let mut replaced = Vec::new(); // keep track of what is replaced in-order
    let mut visited = HashSet::new();

    let mut args: Vec<String> = args.iter().map(ToString::to_string).collect();

    while let Some((command_name, command_len)) = find_command_name(|s| lookup(s).is_some(), &args)
    {
        if command_len > 1 {
            let mut new_args = vec![command_name.to_string()];
            new_args.extend_from_slice(&args[command_len..]);
            args = new_args;
        }
        let alias = lookup(&command_name).unwrap().to_string();
        let bad_alias = || ParseError::MalformedAlias {
            name: command_name.clone(),
            value: alias.to_string(),
        };

        if !visited.insert(command_name.clone()) {
            return Err(ParseError::CircularReference { command_name });
        }
        replaced.push(command_name.clone());

        args = if alias.starts_with("!") {
            // Alias starting with "!" is "shell alias". It is a string that should
            // be passed "as-is" to the shell (after $1/$2/$@ substitutions).
            // Round-trip through shlex::split and shlex::quote is not lossless.
            // Therefore use a different alias handling function that does not
            // use shlex::split.
            expand_shell_alias_args(&args, &alias[1..])
        } else {
            let alias_args: Vec<String> = split(&alias).ok_or_else(bad_alias)?;
            expand_alias_args(&args, alias_args)
        };

        let next_command_name = args.first().cloned().ok_or_else(bad_alias)?;
        if next_command_name == command_name {
            // This allows alias like "log = log -G".
            break;
        }
    }

    Ok((args, replaced))
}

/// Find the longest match of the command name. This checks subcommands in
/// various forms.
///
/// Suppose `foo-bar` is a defined command, then the following
/// arguments can all be used to run the `foo-bar` command:
/// - `["foo-bar"]`
/// - `["foo", "--bar"]`
/// - `["foo", "bar"]`  (only if `foo` does not match)
///
/// Return the command name (For example, `"foo-bar"`), and the number of
/// arguments used (For example, 2 for `["foo", "bar"]`, 1 for `["foo-bar"]`).
///
/// Only consider the first 3 args. This avoids expensive checks.
pub fn find_command_name(
    has_command: impl Fn(&str) -> bool,
    args: &[String],
) -> Option<(String, usize)> {
    // "best" == "longest"
    let mut best_match = None;
    let mut candidate = String::new();

    for (i, arg) in args.iter().enumerate().take(3) {
        if arg.starts_with("--") {
            // Turn ["foo", "--bar"] into "foo-bar".
            candidate += &arg[1..];
        } else if best_match.is_none() {
            // Turn ["foo", "bar"] into "foo-bar", if "foo" does not already match.
            if i > 0 {
                candidate += "-";
            }
            candidate += arg;
        } else {
            break;
        }
        if has_command(&candidate) {
            best_match = Some((candidate.clone(), i + 1))
        }
    }
    best_match
}

/// Expand a single alias.
///
/// The first item of both `command_args` and `alias_args` are expected to be
/// command name.
///
/// Usually returns:
///
/// ```plain,ignore
/// alias_args + command_args[1:]
/// ```
///
/// In case there are `$1`, `$2` etc. in `alias_args`, those parts of
/// `alias_args` will be replaced by corrosponding parts of `command_args`, and
/// the result looks like:
///
/// ```plain,ignore
/// alias_name + alias_args (with $x replaced) + command_args[n+1:]
/// ```
///
/// where `n` is the maximum number occured in `$x`.
fn expand_alias_args(command_args: &[String], alias_args: Vec<String>) -> Vec<String> {
    let mut n = 0;
    let mut args: Vec<String> = alias_args
        .into_iter()
        .map(|a| {
            if a.starts_with("$") {
                if let Ok(i) = a[1..].parse::<usize>() {
                    if let Some(existing_arg) = command_args.get(i) {
                        // Found a substitution. Use it.
                        // Also update the maximum number `n`.
                        n = i.max(n);
                        return existing_arg.to_string();
                    }
                }
            }
            a
        })
        .collect();

    if let Some(slice) = command_args.get(n + 1..) {
        args.extend(slice.iter().cloned());
    } else {
        // TODO: This might be an error case.
    }
    args
}

/// Expand a single shell alias.
///
/// This is similar to `expand_alias_args`, but the "shell alias" is not split,
/// and the rest of `command_args` is not appeneded to the expanded result
/// automatically.
///
/// In theory, this is incorrect in corner cases. Unfortunately it is the only
/// way to preserve Mercurial's behavior.
///
/// Return ["debugrunshell", "--cmd=<shell command>"].
fn expand_shell_alias_args(command_args: &[String], shell_alias: &str) -> Vec<String> {
    // Imitates "aliasinterpolate" in mercurial/dispatch.py

    let mut cmd = String::new();
    let mut buf = String::new();
    let mut arg_index = 1;

    for ch in shell_alias.chars() {
        match (buf.as_ref(), ch) {
            // "$@"
            ("", '"') | ("\"", '$') | ("\"$", '@') => {
                buf.push(ch);
            }
            ("\"$@", '"') => {
                cmd += &command_args
                    .iter()
                    .skip(1)
                    .map(|s| shlex::quote(s))
                    .collect::<Vec<_>>()
                    .join(" ");
                buf.clear();
            }

            // $@, $$, $0 ... $9
            // XXX: Does not support $10 or larger indexes.
            ("", '$') => {
                buf.push(ch);
            }
            ("$", '@') => {
                cmd += &command_args
                    .iter()
                    .skip(1)
                    .cloned()
                    .collect::<Vec<_>>()
                    .join(" ");
                buf.clear();
            }
            ("$", '$') => {
                cmd.push('$');
                buf.clear();
            }
            ("$", i) if i.is_digit(10) => {
                let i: usize = i.to_string().parse().unwrap();
                cmd += &command_args.get(i).cloned().unwrap_or_default();
                buf.clear();
                arg_index = arg_index.max(i);
            }

            // other cases
            _ => {
                cmd += &buf;
                cmd.push(ch);
                buf.clear();
            }
        }
    }
    cmd += &buf;

    vec!["debugrunshell".into(), format!("--cmd={}", cmd)]
}

/// Prefix match commands to their full command name.  If a prefix is not unique an Error::AmbiguousCommand
/// will be returned with a vector of possibilities to choose from.
///
/// * `command_map` - The mapping of a command name ( key ) -> some id ( val ).
/// This is important because some command_names are really the same command
/// e.g. 'id' and 'identify'.  If commands point to the same id they are assumed
/// to be equivalent.
///
/// * `arg` - The command prefix to expand.
///
/// If there is an exact match the argument is returned as-is.
/// If there is no match the argument is returned as-is.
///
/// Commands with negative isize are considered as "debug" commands and treated
/// specially - if there is only one non-debug match, that match will be used
/// without causing `AmbiguousCommand` error.
pub fn expand_prefix(
    command_map: &BTreeMap<String, isize>,
    arg: impl ToString,
) -> Result<String, ParseError> {
    let arg = arg.to_string();
    let resolved = match command_map.get(&arg) {
        Some(_) => arg,
        None => {
            let command_range = command_map.range(get_prefix_bounds(&arg));

            let mut seen_ids = HashSet::new();
            let mut command_matches = HashSet::new();
            let mut non_debug_command_matches = HashSet::new();

            // split commands point to the same handler like id and identify, we only need one
            for (command, id) in command_range {
                if !seen_ids.contains(&id) {
                    command_matches.insert(command.clone());
                    if *id >= 0 {
                        non_debug_command_matches.insert(command.clone());
                    }
                    seen_ids.insert(id);
                }
            }

            let command_matches = if non_debug_command_matches.is_empty() {
                command_matches
            } else {
                non_debug_command_matches
            };

            if command_matches.len() > 1 {
                // Prepare the error message. It's a bit complex due to debug commands and aliases.
                let ids: HashSet<isize> = command_matches
                    .iter()
                    .map(|name| command_map[name])
                    .collect();
                let mut id_to_command_map: HashMap<isize, Vec<String>> = HashMap::new();
                for (name, id) in command_map {
                    if ids.contains(id) && name.starts_with(&arg) {
                        id_to_command_map
                            .entry(*id)
                            .or_insert(Vec::new())
                            .push(name.to_string());
                    }
                }

                // sort command aliases by length for consistency
                for (_, vec) in &mut id_to_command_map {
                    vec.sort_by_key(|s| s.len());
                }

                // join command aliases with ' or ' for better UX
                // e.g. id or identify
                let mut possibilities: Vec<String> = id_to_command_map
                    .into_iter()
                    .map(|(_, vec)| vec.join(" or "))
                    .collect();
                possibilities.sort_unstable();

                return Err(ParseError::AmbiguousCommand {
                    command_name: arg,
                    possibilities,
                });
            } else if command_matches.len() == 1 {
                let alias = command_matches.into_iter().next().unwrap();
                alias.to_string()
            } else {
                arg
            }
        }
    };

    Ok(resolved)
}

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

    #[test]
    fn test_no_alias() {
        let cfg: BTreeMap<&'static str, &'static str> = BTreeMap::new();
        let (_expanded, replaced) = expand_aliases(|x| cfg.get(x), &["log"]).unwrap();
        assert!(replaced.len() == 0);
    }

    #[test]
    fn test_one_alias() {
        let mut cfg = BTreeMap::new();
        cfg.insert("log".to_string(), "log -v".to_string());

        let (expanded, replaced) = expand_aliases(|x| cfg.get(x), &["log"]).unwrap();
        assert_eq!(expanded, vec!["log", "-v"]);
        assert_eq!(replaced, vec!["log"]);
    }

    #[test]
    fn test_ambiguous_alias() {
        let mut command_map = BTreeMap::new();
        command_map.insert("foo".to_string(), 0);
        command_map.insert("foobar".to_string(), 1);

        if let Err(err) = expand_prefix(&command_map, "fo") {
            let msg = format!("{}", err);
            assert_eq!(msg, "command \'fo\' is ambiguous");
        } else {
            panic!()
        }
    }

    #[test]
    fn test_ambiguous_command() {
        let mut command_map = BTreeMap::new();
        command_map.insert("foo".to_string(), 0);
        command_map.insert("foobar".to_string(), 1);

        if let Err(err) = expand_prefix(&command_map, "fo") {
            let msg = format!("{}", err);
            assert_eq!(msg, "command \'fo\' is ambiguous");
        } else {
            panic!()
        }
    }

    #[test]
    fn test_ambiguous_command_and_alias() {
        let mut command_map = BTreeMap::new();
        command_map.insert("foobar".to_string(), 0);
        command_map.insert("foo".to_string(), 1);

        if let Err(err) = expand_prefix(&command_map, "fo") {
            let msg = format!("{}", err);
            assert_eq!(msg, "command \'fo\' is ambiguous");
        } else {
            panic!()
        }
    }

    #[test]
    fn test_command_same_handler() {
        let mut command_map = BTreeMap::new();
        command_map.insert("id".to_string(), 0);
        command_map.insert("identify".to_string(), 0);

        let element = expand_prefix(&command_map, "i").unwrap();
        assert!((element == "id") || (element == "identify"));
    }

    #[test]
    fn test_match_debug_prefix() {
        let mut command_map = BTreeMap::new();
        command_map.insert("debugfoo".to_string(), -1);
        assert_eq!(expand_prefix(&command_map, "d").unwrap(), "debugfoo");
        command_map.insert("dbfoo".to_string(), -1);
        assert_eq!(expand_prefix(&command_map, "d").unwrap(), "dbfoo");

        command_map.insert("debugbar".to_string(), -2);
        assert!(expand_prefix(&command_map, "d").is_err());

        command_map.insert("diff".to_string(), 1);
        assert_eq!(expand_prefix(&command_map, "d").unwrap(), "diff");
        assert!(expand_prefix(&command_map, "debug").is_err());
        assert_eq!(expand_prefix(&command_map, "debugf").unwrap(), "debugfoo");

        command_map.insert("default".to_string(), 2);
        assert!(expand_prefix(&command_map, "d").is_err());
        assert_eq!(expand_prefix(&command_map, "di").unwrap(), "diff");
        assert_eq!(expand_prefix(&command_map, "de").unwrap(), "default");
    }

    #[test]
    fn test_circular_alias() {
        let mut cfg = BTreeMap::new();
        cfg.insert("foo".to_string(), "log".to_string());
        cfg.insert("log".to_string(), "foo".to_string());

        if let Err(err) = expand_aliases(|x| cfg.get(x), &["foo"]) {
            let msg = format!("{}", err);
            assert_eq!(msg, "circular alias: foo");
        } else {
            panic!()
        }
    }

    #[test]
    fn test_long_alias_chain() {
        let mut cfg = BTreeMap::new();
        cfg.insert("a".to_string(), "b 1".to_string());
        cfg.insert("b".to_string(), "c 2".to_string());
        cfg.insert("c".to_string(), "d 3".to_string());

        let (expanded, _replaced) = expand_aliases(|x| cfg.get(x), &["a", "4"]).unwrap();

        assert_eq!(expanded, vec!["d", "3", "2", "1", "4"]);
    }

    #[test]
    fn test_self_alias_chain() {
        let mut cfg = BTreeMap::new();
        cfg.insert("a".to_string(), "b 1".to_string());
        cfg.insert("b".to_string(), "b 2".to_string());
        let (expanded, _replaced) = expand_aliases(|x| cfg.get(x), &["a", "3"]).unwrap();
        assert_eq!(expanded, vec!["b", "2", "1", "3"]);
    }

    // hg --config "alias.foo=log bar" --config alias.bar=oops --config "alias.log=log -v" foo
    // hg foo -> hg log bar -> hg log -v bar ( if bar changes to oops this is invalid )
    #[test]
    fn test_weird_chain() {
        let mut cfg = BTreeMap::new();
        cfg.insert("foo".to_string(), "log bar".to_string());
        cfg.insert("bar".to_string(), "oops".to_string());
        cfg.insert("log".to_string(), "log -v".to_string());

        let (expanded, replaced) = expand_aliases(|x| cfg.get(x), &["foo"]).unwrap();

        assert_eq!(expanded, vec!["log", "-v", "bar"]);
        assert_eq!(replaced, vec!["foo", "log"]);
    }

    // hg --config "alias.foo=foo -r foo -v foo foo" --config "alias.bar=foo" bar
    // hg bar -> hg foo -r foo -v foo foo ( the multiple foos should not be recursively expanded )
    #[test]
    fn test_multiple_commands_in_args() {
        let mut cfg = BTreeMap::new();
        cfg.insert("foo".to_string(), "foo -r foo -v foo foo".to_string());
        cfg.insert("bar".to_string(), "foo".to_string());

        let (expanded, replaced) = expand_aliases(|x| cfg.get(x), &["bar"]).unwrap();

        assert_eq!(expanded, vec!["foo", "-r", "foo", "-v", "foo", "foo"]);
        assert_eq!(replaced, vec!["bar", "foo"]);
    }

    #[test]
    fn test_empty_alias() {
        let mut cfg = BTreeMap::new();
        cfg.insert("nodef".to_string(), "".to_string());

        expand_aliases(|x| cfg.get(x), &["nodef"]).unwrap_err();
    }

    #[test]
    fn test_expand_dollar() {
        let mut cfg = BTreeMap::new();
        cfg.insert("a", "b $2 $1");
        cfg.insert("b", "$1 c d $2");
        cfg.insert("y", "Y");

        // Sufficient args
        let (expanded, _replaced) = expand_aliases(|x| cfg.get(x), &["a", "x", "y", "z"]).unwrap();
        // Initial: a x y z
        // Step 1: Rule: a => b $2 $1 => b y x; Result: b y x z q
        // Step 2: Rule: b => $1 c d $2 => y c d x; Result: y c d x z
        // Step 3: Rule: y => Y; Result: Y c d x z
        assert_eq!(expanded, vec!["Y", "c", "d", "x", "z"]);

        // Insufficient args
        let expanded = expand_aliases(|x| cfg.get(x), &["a", "x"]).unwrap().0;
        assert_eq!(expanded, vec!["$2", "c", "d", "x"]);
    }

    #[test]
    fn test_expand_shell_alias() {
        let expand = |alias: &str, mut args: Vec<&str>| -> String {
            let mut cfg = BTreeMap::new();
            cfg.insert("aliasname", format!("!{}", alias));
            args.insert(0, "aliasname");
            let args = expand_aliases(|x| cfg.get(x), &args).unwrap().0;
            // args = ["debugrunshell", "--cmd=command", ...]
            // Mostly interested in the "--cmd" part.
            args[1][6..].to_string()
        };

        assert_eq!(expand("echo \"foo\"", vec!["bar"]), "echo \"foo\"");
        assert_eq!(expand("echo \"$@\"", vec!["a b", "c"]), "echo \"a b\" c");
        assert_eq!(expand("echo $@", vec!["a b", "c"]), "echo a b c");
        assert_eq!(
            expand("$0 $1 $2", vec!["echo -n", "a b", "c d"]),
            "aliasname echo -n a b"
        );

        assert_eq!(
            expand(
                r#"cat /etc/mercurial/hgrc /etc/mercurial/hgrc.d/*.rc ~/.hgrc "`hg root`/.hg/hgrc" 2>/dev/null"#,
                vec![]
            ),
            r#"cat /etc/mercurial/hgrc /etc/mercurial/hgrc.d/*.rc ~/.hgrc "`hg root`/.hg/hgrc" 2>/dev/null"#
        );
        assert_eq!(
            expand(
                r#"for x in `hg log -r 'only(descendants(.) and bookmark(), master)' --template "{node}\n"`;
            do echo "Changing to $(hg log -r $x -T '{rev} {desc|firstline}')" && hg up $x && $@;
            if [ "$?" != "0" ]; then break; fi;
            done"#,
                vec!["echo", "1"]
            ),
            r#"for x in `hg log -r 'only(descendants(.) and bookmark(), master)' --template "{node}\n"`;
            do echo "Changing to $(hg log -r $x -T '{rev} {desc|firstline}')" && hg up $x && echo 1;
            if [ "$?" != "0" ]; then break; fi;
            done"#
        );
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use thiserror::Error;

#[derive(Debug, Error)]
#[error("invalid arguments\n(use '--help' to get help)")]
pub struct InvalidArguments;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! # CLI Parser
//!
//! CLI Parser is a utility to support the parsing of command-line arguments, commands,
//! subcommands, and flags.
//!
//! # About
//! CLI Parser is used both to declare flags and commands as well as parse command-line arguments
//! in a type-safe way.  Flags can have values associated with them, default values, or no value.
//!
//! # Goals
//! Having a simple, easy-to-use CLI Parser in native code allowing for fast execution, parsing,
//! and validation of command-line arguments.
//!
//! Having the flexibility of being able to dynamically load flags from an external source such
//! as other languages ( python ) or files ( configuration ).
//!

pub mod alias;
pub mod errors;
pub mod macros;
pub mod parser;
pub mod utils;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

#[macro_export]
macro_rules! define_flags {
    ( $( $vis:vis struct $name:ident { $( $token:tt )* } )*  ) => {
        $( $crate::_define_flags_impl!(
            input [ $( $token )* ]
            flags []
            arg0 ()
            args []
            varargs ()
            subflags []
            misc ($vis $name 1 false)
        ); )*
    };
}

#[doc(hidden)]
#[macro_export]
macro_rules! _define_flags_impl {
    // Nothing left to parse
    ( input []
      flags [ $( ($short:literal, $field:ident, $doc:expr, $type:ty, $default:expr, $argtype:literal) )* ]
      arg0 ( $( $arg0:ident )? )
      args [ $( ($arg:ident, $arg_index:tt) )* ]
      varargs ( $($varargs:ident)? )
      subflags [ $( ($subflag_name:ident, $subflag_type:ty) )* ]
      misc ($vis:vis $name:ident $varargs_offset:tt $has_varargs:tt)
    ) => {
        $vis struct $name {
            $( #[doc=$doc] pub $field : $type , )*
            $( pub $varargs: Vec<String>, )?
            $( pub $arg0: String, )?
            $( pub $arg: String, )*
            $( pub $subflag_name: $subflag_type, )*
        }

        impl $crate::parser::StructFlags for $name {
            fn flags() -> Vec<$crate::parser::Flag> {
                let flags: Vec<(char, String, String, $crate::parser::Value, String)> = vec![
                    $( ($short, stringify!($field).replace("r#", "").replace("_", "-"), $doc.trim().to_string(), $crate::parser::Value::from($default), $argtype.to_string()), )*
                ];
                #[allow(unused_mut)]
                let mut result: Vec<$crate::parser::Flag> = flags.into_iter().map(Into::into).collect();
                $( result.append(&mut <$subflag_type>::flags()); )*
                result
            }
        }

        impl TryFrom<$crate::parser::ParseOutput> for $name {
            type Error = ::anyhow::Error;

            fn try_from(out: $crate::parser::ParseOutput) -> ::anyhow::Result<Self> {
                if !$has_varargs && out.args.len() > $varargs_offset {
                    return Err($crate::errors::InvalidArguments.into());
                }
                if !out.args.is_empty() && out.args.len() < $varargs_offset {
                    return Err($crate::errors::InvalidArguments.into());
                }

                Ok(Self {
                    $( $field : out.pick::<$type>(&stringify!($field).replace("r#", "").replace("_", "-")), )*
                    $( $varargs: out.args.get($varargs_offset..).map(|v| v.to_vec()).unwrap_or_default(), )?
                    $( $arg0: out.args.get(0).cloned().unwrap_or_default(), )?
                    $( $arg: out.args[$arg_index].clone(), )*
                    $( $subflag_name: <$subflag_type>::try_from(out.clone_only_opts())?, )*
                })
            }
        }
    };

    // Match a field like:
    //
    //    /// description
    //    name: type,
    ( input [ #[doc=$doc:expr] $field:ident : $type:ty, $($rest:tt)* ]
      flags [ $( $flags:tt )* ]
      arg0 $arg0:tt
      args $args:tt
      varargs $varargs:tt
      subflags $subflags:tt
      misc $misc:tt
    ) => {
        $crate::_define_flags_impl!(
            input [ $( $rest )* ]
            flags [ $( $flags )* (' ', $field, $doc, $type, (<$type>::default()), "") ]
            arg0 $arg0
            args $args
            varargs $varargs
            subflags $subflags
            misc $misc
        );
    };

    // Match a field like:
    //
    //    /// description
    //    #[argtype("type")]
    //    name: type,
    ( input [ #[doc=$doc:expr] #[argtype($argtype:literal)] $field:ident : $type:ty, $($rest:tt)* ]
      flags [ $( $flags:tt )* ]
      arg0 $arg0:tt
      args $args:tt
      varargs $varargs:tt
      subflags $subflags:tt
      misc $misc:tt
    ) => {
        $crate::_define_flags_impl!(
            input [ $( $rest )* ]
            flags [ $( $flags )* (' ', $field, $doc, $type, (<$type>::default()), $argtype) ]
            arg0 $arg0
            args $args
            varargs $varargs
            subflags $subflags
            misc $misc
        );
    };

    // Match a field like:
    //
    //    /// description
    //    #[argtype("type")]
    //    #[short('s')]
    //    name: type,
    ( input [ #[doc=$doc:expr] #[argtype($argtype:literal)] #[short($short:literal)] $field:ident : $type:ty, $($rest:tt)* ]
      flags [ $( $flags:tt )* ]
      arg0 $arg0:tt
      args $args:tt
      varargs $varargs:tt
      subflags $subflags:tt
      misc $misc:tt
    ) => {
        $crate::_define_flags_impl!(
            input [ $( $rest )* ]
            flags [ $( $flags )* ($short, $field, $doc, $type, (<$type>::default()), $argtype) ]
            arg0 $arg0
            args $args
            varargs $varargs
            subflags $subflags
            misc $misc
        );
    };

    // Match a field like:
    //
    //    /// description
    //    name: type = default,
    ( input [ #[doc=$doc:expr] $field:ident : $type:ty = $default:tt, $($rest:tt)* ]
      flags [ $( $flags:tt )* ]
      arg0 $arg0:tt
      args $args:tt
      varargs $varargs:tt
      subflags $subflags:tt
      misc $misc:tt
    ) => {
        $crate::_define_flags_impl!(
            input [ $( $rest )* ]
            flags [ $( $flags )* (' ', $field, $doc, $type, $default, "") ]
            arg0 $arg0
            args $args
            varargs $varargs
            subflags $subflags
            misc $misc
        );
    };

    // Match a field like:
    //
    //    /// description
    //    #[short('s')]
    //    name: type,
    ( input [ #[doc=$doc:expr] #[short($short:literal)] $field:ident : $type:ty, $($rest:tt)* ]
      flags [ $( $flags:tt )* ]
      arg0 $arg0:tt
      args $args:tt
      varargs $varargs:tt
      subflags $subflags:tt
      misc $misc:tt
    ) => {
        $crate::_define_flags_impl!(
            input [ $( $rest )* ]
            flags [ $( $flags )* ($short, $field, $doc, $type, (<$type>::default()), "") ]
            arg0 $arg0
            args $args
            varargs $varargs
            subflags $subflags
            misc $misc
        );
    };

    // Match a field like:
    //
    //    /// description
    //    #[short('s')]
    //    #[argtype("type")]
    //    name: type,
    ( input [ #[doc=$doc:expr] #[short($short:literal)] #[argtype($argtype:literal)] $field:ident : $type:ty, $($rest:tt)* ]
      flags [ $( $flags:tt )* ]
      arg0 $arg0:tt
      args $args:tt
      varargs $varargs:tt
      subflags $subflags:tt
      misc $misc:tt
    ) => {
        $crate::_define_flags_impl!(
            input [ $( $rest )* ]
            flags [ $( $flags )* ($short, $field, $doc, $type, (<$type>::default()), $argtype) ]
            arg0 $arg0
            args $args
            varargs $varargs
            subflags $subflags
            misc $misc
        );
    };

    // Match a field like:
    //
    //    /// description
    //    #[short('s')]
    //    name: type = default,
    ( input [ #[doc=$doc:expr] #[short($short:literal)] $field:ident : $type:ty = $default:tt, $($rest:tt)* ]
      flags [ $( $flags:tt )* ]
      arg0 $arg0:tt
      args $args:tt
      varargs $varargs:tt
      subflags $subflags:tt
      misc $misc:tt
    ) => {
        $crate::_define_flags_impl!(
            input [ $( $rest )* ]
            flags [ $( $flags )* ($short, $field, $doc, $type, $default, "") ]
            arg0 $arg0
            args $args
            varargs $varargs
            subflags $subflags
            misc $misc
        );
    };

    // Match a field like:
    //
    //    #[arg]
    //    name: String,
    ( input [ #[arg] $arg_name:ident : String, $($rest:tt)* ]
      flags $flags:tt
      arg0 $arg0:tt
      args [ $( $rest_args:tt )* ]
      varargs $varargs:tt
      subflags $subflags:tt
      misc ($vis:vis $name:ident $varargs_offset:tt $has_varargs:tt)
    ) => {
        $crate::_define_flags_impl!(
            input [ $( $rest )* ]
            flags $flags
            arg0 $arg0
            args [ ($arg_name, $varargs_offset) $( $rest_args )* ]
            varargs $varargs
            subflags $subflags
            misc ($vis $name ($varargs_offset + 1) $has_varargs)
        );
    };

    // Match a field like:
    //
    //    foo_opts: FooFlags,
    ( input [ $flag_name:ident : $flag_type:ty, $($rest:tt)* ]
      flags $flags:tt
      arg0 $arg0:tt
      args $args:tt
      varargs $varargs:tt
      subflags [ $( $subflags:tt )* ]
      misc $misc:tt
    ) => {
        $crate::_define_flags_impl!(
            input [ $( $rest )* ]
            flags $flags
            arg0 $arg0
            args $args
            varargs $varargs
            subflags [ $( $subflags )* ( $flag_name, $flag_type ) ]
            misc $misc
        );
    };

    // Match a field like:
    //
    //    #[args]
    //    patterns: Vec<String>,
    ( input [ #[args] $varargs_name:ident : Vec<String>, $($rest:tt)* ]
      flags $flags:tt
      arg0 $arg0:tt
      args $args:tt
      varargs ()
      subflags $subflags:tt
      misc ($vis:vis $name:ident $varargs_offset:tt false)
    ) => {
        $crate::_define_flags_impl!(
            input [ $( $rest )* ]
            flags $flags
            arg0 $arg0
            args $args
            varargs ( $varargs_name )
            subflags $subflags
            misc ($vis $name $varargs_offset true)
        );
    };

    // Match a field like:
    //
    //    #[command_name]
    //    command_name: String
    ( input [ #[command_name] $arg0:ident : String, $($rest:tt)* ]
      flags $flags:tt
      arg0 ()
      args $args:tt
      varargs $varargs:tt
      subflags $subflags:tt
      misc $misc:tt
    ) => {
        $crate::_define_flags_impl!(
            input [ $( $rest )* ]
            flags $flags
            arg0 ( $arg0 )
            args $args
            varargs $varargs
            subflags $subflags
            misc $misc
        );
    };

}

#[cfg(test)]
mod tests {
    define_flags! {
        struct TestOptions {
            /// bool value
            boo: bool = true,

            /// foo
            foo: bool,

            /// int value
            count: i64 = 12,

            /// name
            long_name: String = "alice",

            /// revisions
            #[short('r')]
            rev: Vec<String>,
        }

        struct AnotherTestOptions {
            /// follow renames
            follow: bool,

            #[args]
            pats: Vec<String>,

            #[command_name]
            name: String,
        }

        struct ComposedOptions {
            /// new value
            new: bool = true,

            /// (duplicated name)
            foo: bool = false,

            test_opts: TestOptions,

            #[args]
            args: Vec<String>,
        }
    }

    use crate::parser::Flag;
    use crate::parser::ParseOptions;
    use crate::parser::StructFlags;
    use crate::parser::Value;

    #[test]
    fn test_struct_flags() {
        let flags = TestOptions::flags();
        let expected: Vec<Flag> = vec![
            (None, "boo", "bool value", Value::from(true), ""),
            (None, "foo", "foo", Value::from(false), ""),
            (None, "count", "int value", Value::from(12), ""),
            (None, "long-name", "name", Value::from("alice"), ""),
            (Some('r'), "rev", "revisions", Value::from(Vec::new()), ""),
        ]
        .into_iter()
        .map(Into::into)
        .collect();
        assert_eq!(flags, expected);

        let flags = AnotherTestOptions::flags();
        assert_eq!(flags.len(), 1);
    }

    #[test]
    fn test_struct_parse() {
        let parsed = ParseOptions::new()
            .flags(TestOptions::flags())
            .parse_args(&vec!["cmdname", "--count", "3"])
            .unwrap();
        let parsed = TestOptions::try_from(parsed).unwrap();
        assert_eq!(parsed.boo, true);
        assert_eq!(parsed.count, 3);
        assert_eq!(parsed.long_name, "alice");
        assert!(parsed.rev.is_empty());

        let parsed = ParseOptions::new()
            .flags(TestOptions::flags())
            .parse_args(&vec![
                "cmdname",
                "--no-boo",
                "--long-name=bob",
                "--rev=b",
                "-r",
                "a",
            ])
            .unwrap();
        let parsed = TestOptions::try_from(parsed).unwrap();
        assert_eq!(parsed.boo, false);
        assert_eq!(parsed.foo, false);
        assert_eq!(parsed.count, 12);
        assert_eq!(parsed.long_name, "bob");
        assert_eq!(parsed.rev, vec!["b", "a"]);

        let parsed = ParseOptions::new()
            .flags(TestOptions::flags())
            .parse_args(&vec!["--no-boo", "arg0", "arg1"])
            .unwrap();
        // arg1 is unexpected
        assert!(TestOptions::try_from(parsed).is_err());

        let parsed = ParseOptions::new()
            .flags(AnotherTestOptions::flags())
            .parse_args(&vec!["--no-follow", "foo", "b", "--follow", "c"])
            .unwrap();
        let parsed = AnotherTestOptions::try_from(parsed).unwrap();
        assert_eq!(parsed.follow, true);
        assert_eq!(parsed.pats, vec!["b", "c"]);
        assert_eq!(parsed.name, "foo");
    }

    #[test]
    fn test_composed_options() {
        let parsed = ParseOptions::new()
            .flags(ComposedOptions::flags())
            .parse_args(&vec![
                "cmdname",
                "--no-new",
                "--foo=true",
                "1",
                "--count",
                "5",
                "6",
            ])
            .unwrap();
        let parsed = ComposedOptions::try_from(parsed).unwrap();
        assert_eq!(parsed.new, false);
        assert_eq!(parsed.test_opts.count, 5);
        assert_eq!(parsed.test_opts.foo, true);
        assert_eq!(parsed.foo, true);
        assert_eq!(parsed.args, vec!["1", "6"]);
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::borrow::Cow;
use std::collections::BTreeMap;
use std::collections::HashMap;

#[cfg(feature = "python")]
use cpython::*;
#[cfg(feature = "python")]
use cpython_ext::Bytes;
#[cfg(feature = "python")]
use cpython_ext::Str;
use thiserror::Error;

use crate::utils::get_prefix_bounds;

#[derive(Debug, Error)]
pub enum ParseError {
    #[error("option {option_name} not recognized")]
    OptionNotRecognized { option_name: String },
    #[error("option {option_name} requires argument")]
    OptionRequiresArgument { option_name: String },
    #[error("invalid value '{given}' for option {option_name}, expected {expected}")]
    OptionArgumentInvalid {
        option_name: String,
        given: String,
        expected: String,
    },
    #[error("option {option_name} not a unique prefix")]
    OptionAmbiguous {
        option_name: String,
        possibilities: Vec<String>,
    },
    #[error("command '{command_name}' is ambiguous")]
    AmbiguousCommand {
        command_name: String,
        possibilities: Vec<String>,
    },
    #[error("circular alias: {command_name}")]
    CircularReference { command_name: String },
    #[error("alias definition {name} = {value:?} cannot be parsed")]
    MalformedAlias { name: String, value: String },
}

#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub enum Value {
    Bool(Option<bool>),
    Str(String),
    Int(i64),
    List(Vec<String>),
    OptStr(Option<String>),
}

impl Value {
    fn accept(&mut self, token_opt: Option<&str>) -> Result<(), ParseError> {
        let token = match token_opt {
            Some(s) => s,
            None => {
                return Err(ParseError::OptionRequiresArgument {
                    option_name: "".to_string(),
                });
            }
        };

        match self {
            Value::Bool(_) => unreachable!(),
            Value::Str(ref mut s) => {
                *s = token.to_string();
                Ok(())
            }
            Value::OptStr(ref mut s) => {
                *s = Some(token.to_string());
                Ok(())
            }
            Value::Int(ref mut i) => {
                *i = token
                    .parse::<i64>()
                    .map_err(|_| ParseError::OptionArgumentInvalid {
                        option_name: "".to_string(),
                        given: token.to_string(),
                        expected: "int".to_string(),
                    })?;
                Ok(())
            }
            Value::List(ref mut vec) => {
                vec.push(token.to_string());
                Ok(())
            }
        }
    }
}

#[cfg(feature = "python")]
impl ToPyObject for Value {
    type ObjectType = PyObject;

    fn to_py_object(&self, py: Python) -> Self::ObjectType {
        match self {
            Value::Bool(b) => b.to_py_object(py).into_object(),
            Value::Str(s) => Str::from(Bytes::from(s.to_string()))
                .to_py_object(py)
                .into_object(),
            Value::OptStr(s) => s
                .as_ref()
                .map(|s| Str::from(s.to_string()))
                .to_py_object(py),
            Value::Int(i) => i.to_py_object(py).into_object(),
            Value::List(vec) => {
                let collection: Vec<Str> = vec
                    .into_iter()
                    .map(|s: &String| Str::from(Bytes::from(s.to_string())))
                    .collect();
                collection.to_py_object(py).into_object()
            }
        }
    }
}

#[cfg(feature = "python")]
impl<'source> FromPyObject<'source> for Value {
    fn extract(py: Python, obj: &'source PyObject) -> PyResult<Self> {
        if let Ok(b) = obj.cast_as::<PyBool>(py) {
            return Ok(Value::Bool(Some(b.is_true())));
        }

        if let Ok(_l) = obj.cast_as::<PyList>(py) {
            return Ok(Value::List(Vec::new()));
        }
        if let Ok(s) = obj.cast_as::<PyString>(py) {
            return Ok(Value::Str(s.to_string(py).unwrap().to_string()));
        }

        if let Ok(_i) = obj.cast_as::<PyInt>(py) {
            return Ok(Value::Int(obj.extract::<i64>(py).unwrap()));
        }

        Ok(Value::Bool(None))
    }
}

impl From<Value> for i64 {
    fn from(v: Value) -> Self {
        match v {
            Value::Int(i) => i,
            _ => panic!("programming error:  {:?} was converted to i64", v),
        }
    }
}

impl From<Value> for String {
    fn from(v: Value) -> Self {
        match v {
            Value::Str(s) => s,
            _ => panic!("programming error:  {:?} was converted to String", v),
        }
    }
}

impl From<Value> for Option<String> {
    fn from(v: Value) -> Self {
        match v {
            Value::OptStr(s) => s,
            _ => panic!(
                "programming error:  {:?} was converted to Option<String>",
                v
            ),
        }
    }
}

impl From<Value> for bool {
    fn from(v: Value) -> Self {
        match v {
            Value::Bool(b) => b.unwrap_or(false),
            _ => panic!("programming error:  {:?} was converted to bool", v),
        }
    }
}

impl From<Value> for Option<bool> {
    fn from(v: Value) -> Self {
        match v {
            Value::Bool(b) => b,
            _ => panic!("programming error:  {:?} was converted to bool", v),
        }
    }
}

impl From<Value> for Vec<String> {
    fn from(v: Value) -> Self {
        match v {
            Value::List(vec) => vec,
            _ => panic!("programming error:  {:?} was converted to Vec<String>", v),
        }
    }
}

impl From<i64> for Value {
    fn from(v: i64) -> Self {
        Value::Int(v)
    }
}

impl From<bool> for Value {
    fn from(v: bool) -> Self {
        Value::Bool(Some(v))
    }
}

impl From<Option<bool>> for Value {
    fn from(v: Option<bool>) -> Self {
        Value::Bool(v)
    }
}

impl From<&str> for Value {
    fn from(v: &str) -> Self {
        Value::Str(v.to_string())
    }
}

impl From<String> for Value {
    fn from(v: String) -> Self {
        Value::Str(v)
    }
}

impl From<Option<String>> for Value {
    fn from(v: Option<String>) -> Self {
        Value::OptStr(v)
    }
}

impl From<&[&str]> for Value {
    fn from(v: &[&str]) -> Self {
        Value::List(v.iter().map(|s| s.to_string()).collect())
    }
}

impl From<Vec<String>> for Value {
    fn from(v: Vec<String>) -> Self {
        Value::List(v)
    }
}

/// [`Flag`] defines a command line flag, including:
///
/// - Optional short flag name
/// - Long flag name
/// - Description (for help text)
/// - Default value and its type
///
/// Use [`Flag::from`] to create a [`Flag`] from other types.
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Flag {
    /// short_name of a flag i.e. `q`
    short_name: Option<char>,
    /// long_name of a flag i.e. `quiet`
    long_name: Cow<'static, str>,
    /// description of a flag i.e. `silences the output`
    description: Cow<'static, str>,
    /// default value (including its type)
    default_value: Value,
    /// type of the flag (e.g. `TEMPLATE`)
    flag_type: Option<Cow<'static, str>>,
}

/// Convert a tuple to a [`Flag`].
///
/// The tuple is similar to the command flag registration used in hg Python
/// code. It consists of 4 items `(short, long, description, default)`.
///
/// Examples:
///
/// ```
/// # use cliparser::parser::*;
/// let flag: Flag = ('q', "quiet", "silence output", false, "").into();
///
/// // ' ' as short name indicates no short flag name
/// let flag: Flag = (' ', "quiet", "silence output", false, "").into();
///
/// // Alternatively, None can be used.
/// let flag: Flag = (None, "quiet", "silence output", true, "").into();
///
/// // Accept various types.
/// let flag: Flag = (Some('r'), format!("rev"), format!("revisions"), "master", "TEMPLATE").into();
/// let flag: Flag = (Some('r'), "rev", "revisions", &["master", "stable"][..], "").into();
/// let flag: Flag = (None, format!("sleep"), format!("sleep few seconds (default: {})", 1), 1, "FOOBAR").into();
/// ```
impl<S, L, D, V, T> From<(S, L, D, V, T)> for Flag
where
    S: Into<Option<char>>,
    L: Into<Cow<'static, str>>,
    D: Into<Cow<'static, str>>,
    V: Into<Value>,
    T: Into<Cow<'static, str>>,
{
    fn from(tuple: (S, L, D, V, T)) -> Flag {
        let (short_name, long_name, description, default_value, flag_type) = tuple;

        let mut short_name = short_name.into();
        // Translate ' ' to "no short name".
        if Some(' ') == short_name {
            short_name = None;
        }

        let flag_type: Cow<'static, str> = flag_type.into();
        let flag_type = if flag_type.is_empty() {
            None
        } else {
            Some(flag_type)
        };

        Flag {
            short_name,
            long_name: long_name.into(),
            description: description.into(),
            default_value: default_value.into(),
            flag_type,
        }
    }
}

/// Convert [`Flag`] to Python tuple `(short, long, val, desc)`.
#[cfg(feature = "python")]
impl ToPyObject for Flag {
    type ObjectType = PyObject;

    fn to_py_object(&self, py: Python) -> Self::ObjectType {
        let short_name = Str::from(self.short_name.map(|s| s.to_string()).unwrap_or_default());
        let long_name = Str::from(self.long_name.to_string());
        let description = Str::from(self.description.to_string());
        if let Some(flag_type) = &self.flag_type {
            (
                short_name,
                long_name,
                &self.default_value,
                description,
                Str::from(flag_type.to_string()),
            )
                .to_py_object(py)
        } else {
            (short_name, long_name, &self.default_value, description).to_py_object(py)
        }
        .into_object()
    }
}

/// Get flag definitions from a struct. Used by `define_flags!` macro.
pub trait StructFlags {
    fn flags() -> Vec<Flag>;
}

pub struct ParseOptions {
    ignore_prefix: bool,
    early_parse: bool,
    keep_sep: bool,
    error_on_unknown_opts: bool,
    flag_aliases: HashMap<String, String>,
    flags: Vec<Flag>,
}

impl ParseOptions {
    pub fn new() -> Self {
        ParseOptions {
            ignore_prefix: false,
            early_parse: false,
            keep_sep: false,
            error_on_unknown_opts: false,
            flag_aliases: HashMap::new(),
            flags: Vec::new(),
        }
    }

    pub fn ignore_prefix(mut self, ignore_prefix: bool) -> Self {
        self.ignore_prefix = ignore_prefix;
        self
    }

    pub fn early_parse(mut self, early_parse: bool) -> Self {
        self.early_parse = early_parse;
        self
    }

    pub fn keep_sep(mut self, keep_sep: bool) -> Self {
        self.keep_sep = keep_sep;
        self
    }

    pub fn flag_alias(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
        self.flag_aliases
            .insert(key.as_ref().to_string(), value.as_ref().to_string());
        self
    }

    pub fn error_on_unknown_opts(mut self, error_on_unknown_opts: bool) -> Self {
        self.error_on_unknown_opts = error_on_unknown_opts;
        self
    }

    pub fn flags(mut self, flags: Vec<Flag>) -> Self {
        self.flags = flags;
        self
    }

    pub fn into_parser(self) -> Parser {
        Parser::from_options(self)
    }

    pub fn parse_args(self, args: &[impl AsRef<str>]) -> Result<ParseOutput, ParseError> {
        self.into_parser().parse_args(args)
    }
}

/// [`Parser`] keeps flag definitions and uses them to parse string arguments.
pub struct Parser {
    // ParseOptions define the behavior of the parser.
    parsing_options: ParseOptions,

    // Flag indexed by short_name.
    short_map: HashMap<char, usize>,

    // Flag indexed by long_name.
    long_map: BTreeMap<String, usize>,

    // Default parse result.
    opts: HashMap<String, Value>,
}

impl Parser {
    /// Prepare to parse arguments using the provided [`ParseOptions`].
    ///
    /// This function builds up indexes around flag names.
    fn from_options(parsing_options: ParseOptions) -> Self {
        let mut short_map = HashMap::new();
        let mut long_map = BTreeMap::new();
        let mut opts = HashMap::new();

        for (i, flag) in parsing_options.flags.iter().enumerate() {
            if let Some(character) = flag.short_name {
                short_map.insert(character, i);
            }
            long_map.insert(flag.long_name.to_string(), i);

            opts.insert(flag.long_name.to_string(), flag.default_value.clone());
        }

        Parser {
            short_map,
            long_map,
            opts,
            parsing_options,
        }
    }

    /// Entry-point for parsing command line arguments.
    ///
    /// ```
    /// use std::env;
    /// use cliparser::parser::*;
    ///
    /// let env_args: Vec<String> = env::args().collect();
    ///
    /// let flags: Vec<Flag> = vec![
    ///     ('q', "quiet", "silence the output", false, "")
    /// ].into_iter().map(Into::into).collect();
    ///
    /// let parser = ParseOptions::new().flags(flags).into_parser();
    ///
    /// parser.parse_args(&env_args);
    /// ```
    ///
    /// parse_args will clean arguments such that they can be properly parsed by Parser#_parse
    pub fn parse_args(&self, args: &[impl AsRef<str>]) -> Result<ParseOutput, ParseError> {
        let args: Vec<&str> = args.iter().map(AsRef::as_ref).collect();

        let mut first_arg_index = args.len();
        let mut opts = self.opts.clone();
        let mut iter = args.into_iter().enumerate().peekable();
        let mut positional_args = Vec::new();

        let mut set_first_arg_index = |positional_args: &Vec<&str>, i| {
            if positional_args.is_empty() {
                first_arg_index = i;
            }
        };

        while let Some(&(i, arg)) = iter.peek() {
            if arg.eq("--") {
                if !self.parsing_options.keep_sep {
                    let _ = iter.next(); // don't care about -- it's just a separator
                }
                set_first_arg_index(&positional_args, i);
                positional_args.extend(iter.map(|(_i, arg)| arg));
                break;
            } else if arg.eq("-") {
                set_first_arg_index(&positional_args, i);
                positional_args.push(arg);
                iter.next();
            } else if arg.starts_with("--") {
                if let Err(msg) = self.parse_double_hyphen_flag(&mut iter, &mut opts) {
                    if self.parsing_options.error_on_unknown_opts {
                        return Err(msg);
                    } else {
                        set_first_arg_index(&positional_args, i);
                        positional_args.push(arg);
                    }
                }
            } else if arg.starts_with("-") {
                if let Err(msg) = self.parse_single_hyphen_flag(&mut iter, &mut opts) {
                    if self.parsing_options.error_on_unknown_opts {
                        return Err(msg);
                    } else {
                        set_first_arg_index(&positional_args, i);
                        positional_args.push(arg);
                    }
                }
            } else {
                set_first_arg_index(&positional_args, i);
                positional_args.push(arg);
                iter.next();
            }
        }

        Ok(ParseOutput::new(
            opts,
            positional_args.iter().map(|s| s.to_string()).collect(),
            first_arg_index,
        ))
    }

    fn parse_double_hyphen_flag<'a>(
        &self,
        iter: &mut impl Iterator<Item = (usize, &'a str)>,
        opts: &mut HashMap<String, Value>,
    ) -> Result<(), ParseError> {
        let arg = iter.next().unwrap().1;

        debug_assert!(arg.starts_with("--"));
        let arg = &arg[2..];

        let (arg, positive_flag) = if arg.starts_with("no-") {
            (&arg[3..], false)
        } else {
            (arg, true)
        };

        let mut parts = arg.splitn(2, "=");
        let clean_arg = parts.next().unwrap();
        let clean_arg = self
            .parsing_options
            .flag_aliases
            .get(clean_arg)
            .map(|name| name.as_ref())
            .unwrap_or(clean_arg);

        if let Some(&known_flag_id) = self.long_map.get(clean_arg) {
            let name = self.parsing_options.flags[known_flag_id].long_name.as_ref();
            match opts.get_mut(name) {
                Some(Value::Bool(ref mut b)) => *b = Some(positive_flag),
                Some(ref mut value) => {
                    let next = parts.next().or_else(|| iter.next().map(|(_i, arg)| arg));
                    value
                        .accept(next)
                        .map_err(|e| Parser::inject_option_name("--", name, e))?;
                }
                None => unreachable!(),
            }
            return Ok(());
        };

        let flag_with_no: String = "no-".to_string() + clean_arg;

        if let Some(&known_flag_id) = self.long_map.get(&flag_with_no) {
            let name = self.parsing_options.flags[known_flag_id].long_name.as_ref();
            match opts.get_mut(name) {
                Some(Value::Bool(ref mut b)) => *b = Some(!positive_flag),
                Some(ref mut value) => {
                    let next = parts.next().or_else(|| iter.next().map(|(_i, arg)| arg));
                    value
                        .accept(next)
                        .map_err(|e| Parser::inject_option_name("--", name, e))?;
                }
                None => unreachable!(),
            }
            return Ok(());
        }

        if self.parsing_options.ignore_prefix {
            return Err(ParseError::OptionNotRecognized {
                option_name: "--".to_owned() + clean_arg,
            });
        }

        let range = self.long_map.range(get_prefix_bounds(clean_arg));
        let prefixed_flag_ids: Vec<usize> = range.map(|(_, flag)| *flag).collect();

        if prefixed_flag_ids.len() > 1 {
            return Err(ParseError::OptionAmbiguous {
                option_name: "--".to_owned() + clean_arg,
                possibilities: prefixed_flag_ids
                    .into_iter()
                    .map(|i| self.parsing_options.flags[i].long_name.to_string())
                    .collect(),
            });
        } else if prefixed_flag_ids.len() == 0 {
            return Err(ParseError::OptionNotRecognized {
                option_name: "--".to_owned() + clean_arg,
            });
        } else {
            let matched_flag = &self.parsing_options.flags[prefixed_flag_ids[0]];
            let name = matched_flag.long_name.as_ref();
            match opts.get_mut(name) {
                Some(Value::Bool(ref mut b)) => *b = Some(positive_flag),
                Some(ref mut value) => {
                    let next = parts.next().or_else(|| iter.next().map(|(_i, arg)| arg));
                    value
                        .accept(next)
                        .map_err(|e| Parser::inject_option_name("--", name, e))?;
                }
                None => unreachable!(),
            }
            return Ok(());
        }
    }

    fn parse_single_hyphen_flag<'a>(
        &self,
        iter: &mut impl Iterator<Item = (usize, &'a str)>,
        opts: &mut HashMap<String, Value>,
    ) -> Result<(), ParseError> {
        let clean_arg = iter.next().unwrap().1.trim_start_matches("-");

        let mut char_iter = clean_arg.chars().peekable();

        while let Some(curr_char) = char_iter.next() {
            if let Some(&known_flag_id) = self.short_map.get(&curr_char) {
                let flag_name = self.parsing_options.flags[known_flag_id]
                    .long_name
                    .to_string();
                match opts.get_mut(&flag_name) {
                    Some(Value::Bool(ref mut b)) => *b = Some(true),
                    Some(ref mut value) => {
                        if char_iter.peek().is_none() {
                            let next = iter.next().map(|(_i, arg)| arg);
                            value.accept(next).map_err(|e| {
                                Parser::inject_option_name("-", curr_char.to_string().as_ref(), e)
                            })?;
                        } else {
                            let consumed = char_iter.collect::<String>();
                            let consumed = Some(&consumed[..]);
                            value.accept(consumed).map_err(|e| {
                                Parser::inject_option_name("-", curr_char.to_string().as_ref(), e)
                            })?;
                            break;
                        }
                    }
                    None => unreachable!(),
                }
            } else {
                return Err(ParseError::OptionNotRecognized {
                    option_name: "-".to_string() + curr_char.to_string().as_ref(),
                });
            }
            if self.parsing_options.early_parse {
                break;
            }
        }
        Ok(())
    }

    fn inject_option_name(prefix: &str, name: &str, error: ParseError) -> ParseError {
        match error {
            ParseError::OptionNotRecognized { option_name: _ } => ParseError::OptionNotRecognized {
                option_name: prefix.to_string() + name,
            },
            ParseError::OptionRequiresArgument { option_name: _ } => {
                ParseError::OptionRequiresArgument {
                    option_name: prefix.to_string() + name,
                }
            }
            ParseError::OptionArgumentInvalid {
                option_name: _,
                given,
                expected,
            } => ParseError::OptionArgumentInvalid {
                option_name: prefix.to_string() + name,
                given,
                expected,
            },
            ParseError::OptionAmbiguous {
                option_name: _,
                possibilities,
            } => ParseError::OptionAmbiguous {
                option_name: prefix.to_string() + name,
                possibilities,
            },
            err => err,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParseOutput {
    /// The opts
    opts: HashMap<String, Value>,
    /// The positional args
    pub args: Vec<String>,
    pub first_arg_index: usize,
}

/// ParseOutput represents all of the information successfully parsed from the command-line
/// arguments, as well as exposing a convenient API for application logic to query results
/// parsed.
impl ParseOutput {
    pub fn new(opts: HashMap<String, Value>, args: Vec<String>, first_arg_index: usize) -> Self {
        ParseOutput {
            opts,
            args,
            first_arg_index,
        }
    }

    /// Clone only the "options" part.
    pub fn clone_only_opts(&self) -> ParseOutput {
        ParseOutput {
            opts: self.opts.clone(),
            args: Vec::new(),
            first_arg_index: 0,
        }
    }

    /// Get parsed value by name.
    ///
    /// The callsite must make sure the name and type are correct (i.e. they
    /// were provided by `ParseOptions::flags).
    pub fn pick<T: From<Value>>(&self, long_name: &str) -> T {
        self.opts.get(long_name).cloned().map(Into::into).unwrap()
    }

    pub fn opts(&self) -> &HashMap<String, Value> {
        &self.opts
    }

    pub fn args(&self) -> &Vec<String> {
        &self.args
    }

    /// The index of the first positional argument in the original arguments
    /// passed to `Parser::parse_args`.
    /// If there are no positional arguments, return the length of the original
    /// arguments.
    pub fn first_arg_index(&self) -> usize {
        self.first_arg_index
    }
}

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

    fn flags() -> Vec<Flag> {
        vec![
            (
                'q',
                "quiet",
                "silences the output",
                Value::Bool(Some(false)),
                "",
            ),
            (
                'c',
                "config",
                "supply config file",
                Value::List(Vec::new()),
                "",
            ),
            ('h', "help", "get some help", Value::Bool(Some(false)), ""),
            (
                'v',
                "verbose",
                "level of verbosity",
                Value::Bool(Some(false)),
                "",
            ),
            ('r', "rev", "revision hash", Value::Str("".to_string()), ""),
        ]
        .into_iter()
        .map(Into::into)
        .collect()
    }

    #[test]
    fn test_create_parser() {
        let flags = flags();
        let parser = ParseOptions::new().flags(flags).into_parser();

        assert!(parser.short_map.get(&'v').is_some());
        assert!(parser.short_map.get(&'h').is_some());
        assert!(parser.short_map.get(&'c').is_some());
        assert!(parser.short_map.get(&'q').is_some());

        assert!(parser.long_map.get("verbose").is_some());
        assert!(parser.long_map.get("help").is_some());
        assert!(parser.long_map.get("config").is_some());
        assert!(parser.long_map.get("quiet").is_some());

        assert!(parser.short_map.get(&'t').is_none());
        assert!(parser.long_map.get("random").is_none());
    }

    #[test]
    fn test_parse_single_no_value_flag() {
        let flag = ('q', "quiet", "silences the output", false, "").into();
        let flags = vec![flag];
        let parser = ParseOptions::new().flags(flags).into_parser();
        let mut opts = parser.opts.clone();

        let args = vec!["-q"];

        let _ = parser
            .parse_single_hyphen_flag(&mut args.into_iter().enumerate().peekable(), &mut opts)
            .unwrap();
        let quiet: bool = opts.get("quiet").cloned().unwrap().into();
        assert!(quiet);
    }

    #[test]
    fn test_parse_single_value_flag() {
        let flag = ('c', "config", "supply config file", "", "").into();
        let flags = vec![flag];
        let parser = ParseOptions::new().flags(flags).into_parser();
        let mut opts = parser.opts.clone();
        const PATH: &str = "$HOME/path/to/config/file";

        let args = vec!["-c", PATH];

        let _result = parser
            .parse_single_hyphen_flag(&mut args.into_iter().enumerate().peekable(), &mut opts);
    }

    #[test]
    fn test_parse_single_cluster_with_end_value() {
        let parser = ParseOptions::new().flags(flags()).into_parser();
        let mut opts = parser.opts.clone();
        const PATH: &str = "$HOME/path/to/config/file";
        const CLUSTER: &str = "-qhvc";

        let clustered_args = vec![CLUSTER, PATH];

        let _ = parser
            .parse_single_hyphen_flag(
                &mut clustered_args.into_iter().enumerate().peekable(),
                &mut opts,
            )
            .unwrap();

        //assert_eq!(v.len(), CLUSTER.len() - 1);
    }

    #[test]
    fn test_parse_long_single_no_value() {
        let flag = ('q', "quiet", "silences the output", false, "").into();
        let flags = vec![flag];
        let parser = ParseOptions::new().flags(flags).into_parser();
        let mut opts = parser.opts.clone();

        let args = vec!["--quiet"];

        let _ = parser
            .parse_double_hyphen_flag(&mut args.into_iter().enumerate().peekable(), &mut opts)
            .unwrap();

        //assert_eq!(parsed_flag, flag.long_name);
    }

    #[test]
    fn test_parse_long_single_with_value() {
        let flag = ('c', "config", "supply config file", "", "").into();
        let flags = vec![flag];
        let parser = ParseOptions::new().flags(flags).into_parser();
        let mut opts = parser.opts.clone();
        const PATH: &str = "$HOME/path/to/config/file";

        let args = vec!["--config", PATH];

        let _ = parser
            .parse_double_hyphen_flag(&mut args.into_iter().enumerate().peekable(), &mut opts)
            .unwrap();

        //assert_eq!(parsed_flag, flag.long_name);
        //let s: String = value.clone().try_into().unwrap();
        //assert_eq!(s, PATH.to_string());
    }

    #[test]
    fn test_parse_long_single_int_value() {
        let flag = ('n', "number", "supply a number", 0, "").into();
        let flags = vec![flag];
        let parser = ParseOptions::new().flags(flags).into_parser();
        let mut opts = parser.opts.clone();

        let args = vec!["--number", "60"];

        let _ = parser
            .parse_double_hyphen_flag(&mut args.into_iter().enumerate().peekable(), &mut opts)
            .unwrap();

        //assert_eq!(parsed_flag, flag.long_name);
        //let i: i64 = value.clone().try_into().unwrap();
        //assert_eq!(i, 60);
    }

    #[test]
    fn test_parse_long_single_list_value() {
        let flag = ('n', "number", "supply a list of numbers", &[][..], "").into();
        let flags = vec![flag];
        let parser = ParseOptions::new().flags(flags).into_parser();

        let args = vec![
            "--number".to_string(),
            "60".to_string(),
            "--number".to_string(),
            "59".to_string(),
            "foo".to_string(),
            "--number".to_string(),
            "3".to_string(),
            "bar".to_string(),
        ];

        let result = parser.parse_args(&args).unwrap();

        assert_eq!(result.first_arg_index(), 4);

        let list: Vec<String> = result.pick("number");

        assert_eq!(list, vec!["60", "59", "3"]);
    }

    #[test]
    fn test_parse_long_and_short_single_list_value() {
        let flag = ('n', "number", "supply a list of numbers", &[][..], "").into();
        let flags = vec![flag];
        let parser = ParseOptions::new().flags(flags).into_parser();

        let args = vec![
            "--number", "60", "--number", "59", "-n", "3", "-n5", "foo", "bar",
        ];

        let result = parser.parse_args(&args).unwrap();

        assert_eq!(result.first_arg_index(), 7);

        let list: Vec<String> = result.pick("number");

        assert_eq!(list, vec!["60", "59", "3", "5"]);
    }

    #[test]
    fn test_parse_cluster_with_attached_value() {
        let parser = ParseOptions::new().flags(flags()).into_parser();

        let args = vec!["-qhvcPATH/TO/FILE"];

        let result = parser.parse_args(&args).unwrap();

        let config_path: Vec<String> = result.pick("config");

        assert!(result.opts.get("quiet").is_some());
        assert!(result.opts.get("help").is_some());
        assert!(result.opts.get("verbose").is_some());

        assert_eq!(config_path[0], "PATH/TO/FILE".to_string());
    }

    #[test]
    fn test_parse_cluster_with_attached_value_first() {
        let parser = ParseOptions::new().flags(flags()).into_parser();

        let args = vec!["-cqhv"];

        let result = parser.parse_args(&args).unwrap();

        let config_path: Vec<String> = result.pick("config");

        result.pick::<Value>("quiet");
        result.pick::<Value>("help");
        result.pick::<Value>("verbose");

        assert_eq!(config_path[0], "qhv".to_string());
    }

    #[test]
    fn test_parse_after_double_hyphen() {
        let parser = ParseOptions::new().flags(flags()).into_parser();

        let args = vec!["-q", "--", "-v", "--", "-h"];

        let result = parser.parse_args(&args).unwrap();

        result.pick::<Value>("quiet");
        result.pick::<Value>("help");
        result.pick::<Value>("verbose");

        let pos_args = vec!["-v", "--", "-h"];

        assert_eq!(pos_args, result.args);
    }

    #[test]
    fn test_parse_equals_in_value() {
        let flag = ('c', "config", "supply a config file", "", "").into();
        let flags = vec![flag];
        let parser = ParseOptions::new().flags(flags).into_parser();

        let args = vec!["--config=--config=foo.bar"];

        let result = parser.parse_args(&args).unwrap();

        let config_val: String = result.pick("config");

        assert_eq!("--config=foo.bar", config_val);
    }

    #[test]
    fn test_parse_list_equals_in_values() {
        let flag = ('c', "config", "supply multiple config files", &[][..], "").into();
        let flags = vec![flag];
        let parser = ParseOptions::new().flags(flags).into_parser();

        let args = vec![
            "--config=--config=foo.bar",
            "--config",
            "-c=some.value.long",
            "--config=--config=bar.foo",
        ];

        let result = parser.parse_args(&args).unwrap();

        let config_values: Vec<String> = result.pick("config");

        assert_eq!(
            config_values,
            vec!["--config=foo.bar", "-c=some.value.long", "--config=bar.foo"]
        );
    }

    #[test]
    fn test_parse_list_short_name_with_equals_in_value() {
        let flag = ('c', "config", "supply multiple config files", "", "").into();
        let flags = vec![flag];
        let parser = ParseOptions::new().flags(flags).into_parser();

        let args = vec!["-c=--config.prop=63"];

        let result = parser.parse_args(&args).unwrap();

        let config_value: String = result.pick("config");

        assert_eq!(config_value, "=--config.prop=63");
    }

    #[test]
    fn test_parse_list_mixed_with_spaces_and_equals() {
        let parser = ParseOptions::new().flags(flags()).into_parser();

        let args = vec![
            "log",
            "--rev",
            ".",
            "--config=--rev=e45ab",
            "-c",
            "--rev=test",
            "--",
            "arg",
        ];

        let result = parser.parse_args(&args).unwrap();

        assert_eq!(result.first_arg_index(), 0);

        let config_values: Vec<String> = result.pick("config");

        let rev_value: String = result.pick("rev");

        assert_eq!(config_values, vec!["--rev=e45ab", "--rev=test"]);

        assert_eq!(rev_value, ".");
    }

    #[test]
    fn test_parse_flag_with_value_last_token() {
        let parser = ParseOptions::new().flags(flags()).into_parser();

        let args = vec!["--rev"];

        let result = parser.parse_args(&args).unwrap();

        let rev_value: String = result.pick("rev");

        assert_eq!(rev_value, "");
        // TODO for now this is expected to be the default flag val, but later a Value
        // expecting flag probably should error for the user perhaps -- depends on the current
        // CLI parsing
    }

    #[test]
    fn test_template_value_long_str_value() {
        let flag = ('T', "template", "specify a template", "", "").into();
        let flags = vec![flag];
        let parser = ParseOptions::new().flags(flags).into_parser();

        let template_str = "hg bookmark -ir {node} {tag};\\n";
        // target command is `hg tags -T "hg bookmark -ir {node} {tag};\n"`
        // taken from hg/tests/test-rebase-bookmarks.t

        let args = vec!["tags", "-T", template_str];

        let result = parser.parse_args(&args).unwrap();

        let template_val: String = result.pick("template");

        assert_eq!(template_val, template_str);
    }

    #[test]
    #[should_panic]
    fn test_type_mismatch_try_into_list_panics() {
        let parser = ParseOptions::new().flags(flags()).into_parser();

        let args = vec!["--rev", "test"];

        let result = parser.parse_args(&args).unwrap();

        let _: Vec<String> = result.pick("rev");
        // This is either a definition error (incorrectly configured) or
        // a programmer error at the callsite ( mismatched types ).
    }

    #[test]
    #[should_panic]
    fn test_type_mismatch_try_into_str_panics() {
        let parser = ParseOptions::new().flags(flags()).into_parser();

        let args = vec!["--config", "some value"];

        let result = parser.parse_args(&args).unwrap();

        let _: String = result.pick("config");
        // This is either a definition error (incorrectly configured) or
        // a programmer error at the callsite ( mismatched types ).
    }

    #[test]
    #[should_panic]
    fn test_type_mismatch_try_into_int_panics() {
        let parser = ParseOptions::new().flags(flags()).into_parser();

        let args = vec!["--rev", "test"];

        let result = parser.parse_args(&args).unwrap();

        let _: i64 = result.pick("rev");
        // This is either a definition error (incorrectly configured) or
        // a programmer error at the callsite ( mismatched types ).
    }

    #[test]
    #[should_panic]
    fn test_type_mismatch_try_into_bool_panics() {
        let parser = ParseOptions::new().flags(flags()).into_parser();

        let args = vec!["--rev", "test"];

        let result = parser.parse_args(&args).unwrap();

        let _: bool = result.pick("rev");
        // This is either a definition error (incorrectly configured) or
        // a programmer error at the callsite ( mismatched types ).
    }

    #[test]
    fn test_trailing_equals_sign_double_flag() {
        let parser = ParseOptions::new().flags(flags()).into_parser();

        let args = vec!["--config="];

        let result = parser.parse_args(&args).unwrap();

        let configs: Vec<String> = result.pick("config");
        assert_eq!(configs.len(), 1);
        assert_eq!(configs.get(0).unwrap(), "");
    }

    #[test]
    fn test_prefix_match_double_flag() {
        let parser = ParseOptions::new().flags(flags()).into_parser();

        let args = vec!["--con", "test"];

        let result = parser.parse_args(&args).unwrap();

        let configs: Vec<String> = result.pick("config");
        assert_eq!(configs.len(), 1);
        assert_eq!(configs.get(0).unwrap(), "test");
    }

    #[test]
    fn test_prefix_match_trailing_equals() {
        let parser = ParseOptions::new().flags(flags()).into_parser();

        let args = vec!["--con="];

        let result = parser.parse_args(&args).unwrap();

        let configs: Vec<String> = result.pick("config");
        assert_eq!(configs.len(), 1);
        assert_eq!(configs.get(0).unwrap(), "");
    }

    #[test]
    fn test_prefix_match_ambiguous() {
        let flags = vec![
            (
                'c',
                "config",
                "config overrides",
                Value::List(Vec::new()),
                "",
            ),
            (
                ' ',
                "configfile",
                "config files",
                Value::List(Vec::new()),
                "",
            ),
        ]
        .into_iter()
        .map(Into::into)
        .collect();
        let parser = ParseOptions::new().flags(flags).into_parser();

        let args = vec!["--co="]; // this is an ambiguous flag

        let result = parser.parse_args(&args).unwrap();

        let configs: Vec<String> = result.pick("config");
        let configfiles: Vec<String> = result.pick("configfile");
        assert_eq!(configs.len(), 0);
        assert_eq!(configfiles.len(), 0);
    }

    #[test]
    fn test_prefix_match_mixed_with_exact_match_and_short_flags() {
        let parser = ParseOptions::new().flags(flags()).into_parser();

        let args = vec![
            "--c=",
            "--config",
            "section.key=val",
            "-c=",
            "--conf=section.key=val",
        ];

        let result = parser.parse_args(&args).unwrap();

        assert_eq!(result.first_arg_index(), 5);

        let configs: Vec<String> = result.pick("config");

        let expected = vec!["", "section.key=val", "=", "section.key=val"];

        assert_eq!(configs, expected);
    }

    #[test]
    fn test_no_prefix_match() {
        let args = vec!["--conf", "section.key=val"];
        let result = ParseOptions::new()
            .ignore_prefix(true)
            .flags(flags())
            .parse_args(&args)
            .unwrap();

        let configs: Vec<String> = result.pick("config");

        assert_eq!(configs.len(), 0);
    }

    #[test]
    fn test_aliased_option() {
        let parser = ParseOptions::new()
            .flag_alias("conf", "config")
            .flags(flags())
            .ignore_prefix(true)
            .into_parser();

        let args = vec!["--shallow", "--conf", "section.key=val"];

        let result = parser.parse_args(&args).unwrap();

        let configs: Vec<String> = result.pick("config");

        assert_eq!(configs, vec!["section.key=val"]);
    }

    #[test]
    fn test_early_parse() {
        let parser = ParseOptions::new()
            .early_parse(true)
            .ignore_prefix(true)
            .flags(flags())
            .into_parser();

        let args = vec!["-qc."];

        let result = parser.parse_args(&args).unwrap();

        let configs: Vec<String> = result.pick("config");

        assert_eq!(configs.len(), 0);
    }

    #[test]
    fn test_keep_sep() {
        let parser = ParseOptions::new()
            .early_parse(true)
            .ignore_prefix(true)
            .keep_sep(true)
            .flags(flags())
            .into_parser();

        let args = vec!["--", "-1", "4"];

        let result = parser.parse_args(&args).unwrap();

        assert_eq!(result.first_arg_index(), 0);

        let parsed_args = result.args().clone();

        assert_eq!(parsed_args, vec!["--", "-1", "4"]);
    }

    #[test]
    fn test_parse_flag_starting_with_no_with_positive_arg() {
        let flags = vec![(
            ' ',
            "no-commit",
            "leaves the changes in the working copy",
            Value::Bool(Some(false)),
            "",
        )];
        let flags = flags.into_iter().map(Into::into).collect();
        let parser = ParseOptions::new().flags(flags).into_parser();

        let args = vec!["--commit"];

        let result = parser.parse_args(&args).unwrap();

        if let Value::Bool(Some(no_commit)) = result.pick("no-commit") {
            assert!(!no_commit);
        } else {
            assert!(false);
        }
    }

    #[test]
    fn test_parse_flag_starting_with_no_with_negative_arg() {
        let flags = vec![(
            ' ',
            "no-commit",
            "leaves the changes in the working copy",
            Value::Bool(Some(false)),
            "",
        )];
        let flags = flags.into_iter().map(Into::into).collect();
        let parser = ParseOptions::new().flags(flags).into_parser();

        let args = vec!["--no-commit"];

        let result = parser.parse_args(&args).unwrap();

        if let Value::Bool(Some(no_commit)) = result.pick("no-commit") {
            assert!(no_commit);
        } else {
            assert!(false);
        }
    }

    #[test]
    fn test_no_arg_for_no_boolean() {
        // XXX: --no-foo should not affect non-boolean values.
        let flags = vec![(None, "foo", "foo desc", "", "").into()];
        let parsed = ParseOptions::new()
            .flags(flags)
            .parse_args(&["--no-foo", "bar"])
            .unwrap();
        let foo: String = parsed.pick("foo");
        assert_eq!(foo, "bar");
    }

    #[test]
    fn test_no_flag_for_no_boolean() {
        // XXX: --foo should not affect non-boolean values.
        let flags = vec![(None, "no-foo", "foo desc", "", "").into()];
        let parsed = ParseOptions::new()
            .flags(flags)
            .parse_args(&["--foo", "bar"])
            .unwrap();
        let foo: String = parsed.pick("no-foo");
        assert_eq!(foo, "bar");
    }

    #[test]
    fn test_parse_option_string_value() {
        let flags = vec![
            (
                ' ',
                "opt_str",
                "an optional string",
                Value::OptStr(None),
                "",
            )
                .into(),
        ];
        let parser = ParseOptions::new().flags(flags).into_parser();

        let args: Vec<&str> = Default::default();
        let parsed = parser.parse_args(&args).unwrap();
        assert_eq!(parsed.pick::<Option<String>>("opt_str"), None);

        let parsed = parser.parse_args(&["--opt_str", "foo"]).unwrap();
        assert_eq!(
            parsed.pick::<Option<String>>("opt_str"),
            Some("foo".to_string())
        );
    }

    #[test]
    fn test_parse_option_bool_value() {
        let flags = vec![(None, "opt-bool", "a bool", Value::Bool(None), "").into()];
        let parser = ParseOptions::new().flags(flags).into_parser();

        let args: Vec<&str> = Default::default();
        let parsed = parser.parse_args(&args).unwrap();
        assert_eq!(parsed.pick::<Option<bool>>("opt-bool"), None);

        let parsed = parser.parse_args(&["--opt-bool"]).unwrap();
        assert_eq!(parsed.pick::<Option<bool>>("opt-bool"), Some(true),);

        let parsed = parser.parse_args(&["--no-opt-bool"]).unwrap();
        assert_eq!(parsed.pick::<Option<bool>>("opt-bool"), Some(false),);
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::char;
use std::ops::Range;

pub fn get_prefix_bounds(prefix: impl AsRef<str>) -> Range<String> {
    let mut upper = prefix.as_ref().to_string();
    assert!(!upper.is_empty());

    let mut last_char = upper.pop().unwrap();
    let mut last_char_code: u32 = last_char as u32;

    while let Some(next_val) = last_char_code.checked_add(1) {
        if let Some(c) = char::from_u32(next_val) {
            last_char = c;
            break;
        }
        last_char_code = next_val;
    }

    upper.push(last_char);

    prefix.as_ref().to_string()..upper
}

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

    #[test]
    fn test_get_prefix_bounds_one_letter() {
        let prefix = "a";
        let end = "b";
        let range = get_prefix_bounds(prefix);
        assert_eq!(range.start.as_str(), prefix);
        assert_eq!(range.end.as_str(), end);
    }

    #[test]
    fn test_get_prefix_bounds_ending_with_z() {
        let prefix = "z";
        let end = "{";
        let range = get_prefix_bounds(prefix);
        assert_eq!(range.start.as_str(), prefix);
        assert_eq!(range.end.as_str(), end);
    }

    #[test]
    fn test_get_prefix_bounds_multiple() {
        let prefix = "comm"; // prefix of commit
        let end = "comn";
        let range = get_prefix_bounds(prefix);
        assert_eq!(range.start.as_str(), prefix);
        assert_eq!(range.end.as_str(), end);
    }

    #[test]
    fn test_get_prefix_bounds_ending_space() {
        let prefix = "comm "; // prefix of commit with trailing space
        let end = "comm!";
        let range = get_prefix_bounds(prefix);
        assert_eq!(range.start.as_str(), prefix);
        assert_eq!(range.end.as_str(), end);
    }

    #[test]
    fn test_get_prefix_bounds_unicode() {
        let prefix = "\u{1F36A}"; // Cookie Emoji
        let end = "\u{1F36B}";
        let range = get_prefix_bounds(prefix);
        assert_eq!(range.start.as_str(), prefix);
        assert_eq!(range.end.as_str(), end);
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::env;
use std::ffi::OsStr;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use async_runtime::block_unless_interrupted as block_on;
use configmodel::Config;
use configmodel::ConfigExt;
use manifest_tree::Manifest;
use manifest_tree::TreeManifest;
use repo::repo::Repo;
use termlogger::TermLogger;
use tracing::instrument;
use treestate::treestate::TreeState;
use types::HgId;
use types::RepoPath;
use util::errors::IOContext;
use util::errors::IOError;
use util::file::atomic_write;
use util::path::absolute;
use util::path::expand_path;
use vfs::VFS;

pub fn get_default_destination_directory(config: &dyn Config) -> Result<PathBuf> {
    Ok(absolute(
        if let Some(default_dir) = config.get("clone", "default-destination-dir") {
            expand_path(default_dir)
        } else {
            env::current_dir()?
        },
    )?)
}

pub fn get_default_eden_backing_directory(config: &dyn Config) -> Result<Option<PathBuf>> {
    let legacy_dir = config
        .get("edenfs", "legacy-backing-repos-dir")
        .map(expand_path);
    if let Some(legacy_dir) = legacy_dir {
        if legacy_dir.exists() {
            return Ok(Some(legacy_dir));
        }
    }
    Ok(config.get("edenfs", "backing-repos-dir").map(expand_path))
}

#[derive(Debug, thiserror::Error)]
pub enum WorkingCopyError {
    #[error("No such checkout target '{0}'")]
    NoSuchTarget(HgId),

    #[error(transparent)]
    Io(#[from] IOError),

    #[error(transparent)]
    Other(#[from] anyhow::Error),
}

#[instrument(skip(logger), err)]
pub fn init_working_copy(
    logger: &mut TermLogger,
    repo: &mut Repo,
    target: Option<HgId>,
    sparse_profiles: Vec<String>,
) -> Result<(), WorkingCopyError> {
    if !sparse_profiles.is_empty() {
        let mut sparse_contents: Vec<u8> = Vec::new();
        for profile in &sparse_profiles {
            write!(&mut sparse_contents, "%include {}\n", profile)
                .io_context("error generating sparse contents")?;
        }
        atomic_write(&repo.dot_hg_path().join("sparse"), |f| {
            f.write_all(&sparse_contents)
        })?;
    }

    let target = match target {
        Some(t) => t,
        None => {
            // Nothing to check out - init empty dirstate and bail.
            let mut ts = create_treestate(repo.dot_hg_path())?;
            checkout::clone::flush_dirstate(
                repo.config(),
                &mut ts,
                repo.dot_hg_path(),
                types::hgid::NULL_ID,
            )?;
            return Ok(());
        }
    };

    let roots = repo.dag_commits()?.read().to_dyn_read_root_tree_ids();
    let tree_id = match block_on(roots.read_root_tree_ids(vec![target.clone()]))
        .io_context("error blocking on read_root_tree_ids")??
        .into_iter()
        .next()
    {
        Some((_, tree_id)) => tree_id,
        None => return Err(WorkingCopyError::NoSuchTarget(target)),
    };

    let tree_store = repo.tree_store()?;
    let file_store = repo.file_store()?;

    let source_mf = TreeManifest::ephemeral(tree_store.clone());
    let target_mf = TreeManifest::durable(tree_store.clone(), tree_id.clone());

    for profile in &sparse_profiles {
        let path = RepoPath::from_str(profile).map_err(|e| anyhow!(e))?;
        if matches!(target_mf.get(path)?, None) {
            logger.warn(format!(
                "The profile '{profile}' does not exist. Check out a commit where it exists, or remove it with '@prog@ sparse disableprofile'."
            ));
        }
    }

    let mut ts = create_treestate(repo.dot_hg_path())?;

    match checkout::clone::checkout(
        repo.config(),
        repo.dot_hg_path(),
        &source_mf,
        &target_mf,
        file_store.clone(),
        &mut ts,
        target,
        repo.locker(),
    ) {
        Ok(stats) => {
            logger.info(format!("{}", stats));

            Ok(())
        }
        Err(err) => {
            if err.resumable {
                logger.info(format!(
                    "Checkout failed. Resume with '{} checkout --continue'",
                    logger.cli_name(),
                ));
            }

            Err(err.source.into())
        }
    }
}

fn create_treestate(dot_hg_path: &Path) -> Result<TreeState> {
    let ts_dir = dot_hg_path.join("treestate");
    Ok(TreeState::new(
        &ts_dir,
        VFS::new(dot_hg_path.to_path_buf())?.case_sensitive(),
    )?
    .0)
}

#[derive(Debug, thiserror::Error)]
pub enum EdenCloneError {
    #[error("Failed cloning eden checkout\n Stdout: '{0}'\n Stderr: '{1}'")]
    ExeuctionFailure(String, String),
    #[error("edenfs.command config is not set")]
    MissingCommandConfig(),
}

#[instrument(err)]
pub fn eden_clone(backing_repo: &Repo, working_copy: &Path, target: Option<HgId>) -> Result<()> {
    let config = backing_repo.config();
    let eden_command = config.get_opt::<String>("edenfs", "command")?;
    let mut clone_command = match eden_command {
        Some(cmd) => Command::new(cmd),
        None => return Err(EdenCloneError::MissingCommandConfig().into()),
    };

    // allow tests to specify different configuration directories from prod defaults
    if let Some(base_dir) = config.get_opt::<PathBuf>("edenfs", "basepath")? {
        clone_command.args([
            "--config-dir".into(),
            base_dir.join("eden"),
            "--etc-eden-dir".into(),
            base_dir.join("etc_eden"),
            "--home-dir".into(),
            base_dir.join("home"),
        ]);
    }

    clone_command.args([
        OsStr::new("clone"),
        backing_repo.path().as_os_str(),
        working_copy.as_os_str(),
    ]);

    if config.get_or_default("experimental", "windows-symlinks")? {
        clone_command.args(["--enable-windows-symlinks".to_string()]);
    }

    if let Some(rev) = target {
        clone_command.args(["-r", &rev.to_hex()]);
    } else {
        clone_command.arg("--allow-empty-repo");
    }

    tracing::info!(?clone_command, "running edenfsctl clone");

    let output = clone_command
        .output()
        .with_context(|| format!("failed to execute {:?}", clone_command))?;

    if !output.status.success() {
        return Err(EdenCloneError::ExeuctionFailure(
            String::from_utf8_lossy(&output.stdout).to_string(),
            String::from_utf8_lossy(&output.stderr).to_string(),
        )
        .into());
    }

    if String::from_utf8_lossy(&output.stdout)
        .to_string()
        .contains("edenfs daemon is not currently running")
    {
        tracing::debug!(target: "clone_info", edenfs_started_at_clone="true");
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;
    use std::fs;

    use tempfile::TempDir;

    use super::*;

    #[test]
    pub fn test_get_target_dir() -> Result<()> {
        let tmpdir = TempDir::new()?;
        let mut config: BTreeMap<String, String> = BTreeMap::new();

        // Test with non-set default destination directory
        assert_eq!(
            get_default_destination_directory(&config)?,
            env::current_dir()?.as_path()
        );

        // Test setting default destination directory
        let path = tmpdir.path().join("foo").join("bar");
        config.insert(
            "clone.default-destination-dir".to_string(),
            path.to_str().unwrap().to_string(),
        );
        assert_eq!(get_default_destination_directory(&config).unwrap(), path,);

        Ok(())
    }

    #[test]
    pub fn test_get_eden_backing_dir() -> Result<()> {
        let tmpdir = TempDir::new()?;
        let mut config: BTreeMap<String, String> = BTreeMap::new();
        let legacy_dir = tmpdir.path().join("legacy-dir");
        let new_dir = tmpdir.path().join("new-dir");
        config.insert(
            "edenfs.legacy-backing-repos-dir".to_string(),
            legacy_dir.to_string_lossy().to_string(),
        );
        config.insert(
            "edenfs.backing-repos-dir".to_string(),
            new_dir.to_string_lossy().to_string(),
        );
        // if legacy directory does not exist, use new directory
        assert_eq!(get_default_eden_backing_directory(&config)?, Some(new_dir),);
        fs::create_dir(legacy_dir.clone())?;
        // if legacy directory does exist, use legacy directory
        assert_eq!(
            get_default_eden_backing_directory(&config)?,
            Some(legacy_dir),
        );
        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::io::IsTerminal;

use configmodel::Config;
use configmodel::ConfigExt;
use udsipc::pool;

use crate::ipc::Client;
use crate::ipc::CommandEnv;
use crate::ipc::ProcessProps;
use crate::ipc::ServerIpc;
use crate::spawn;
use crate::util;

/// Connect to a server to run a command. Returns exit code.
///
/// Error when no compatible server can be connected.
/// Spawn new servers on demand.
pub fn run_via_commandserver(args: Vec<String>, config: &dyn Config) -> anyhow::Result<i32> {
    let (should, reason) = should_run_remotely(&args);
    if !should {
        tracing::debug!("skipped using commandserver: {}", reason);
        anyhow::bail!("skipped using commandserver: {}", reason);
    }

    // For now, the server does not fork and can only be used with "exclusive".
    let exclusive = true;
    let dir = util::runtime_dir()?;
    let ipc = match pool::connect(&dir, exclusive) {
        Err(e) => {
            tracing::debug!("no server to connect:\n{:?}", &e);
            if pool::list_uds_paths(&dir).next().is_none() {
                // No servers are running. Spawn a pool of servers.
                let pool_size = config.get_or::<usize>("commandserver", "pool-size", || 2)?;
                let _ = spawn::spawn_pool(pool_size);
            }
            return Err(e.into());
        }
        Ok(ipc) => {
            // Going to consume one server, so spawn another one.
            let _ = spawn::spawn_one();
            ipc
        }
    };

    tracing::debug!("sending stdio to server");
    ipc.send_stdio()?;

    // Check if the server is compatible.
    let client = Client { ipc };
    let props: ProcessProps = ServerIpc::process_props(&client)?;
    if let Some(server_groups) = props.groups {
        if let Some(client_groups) = util::groups() {
            if server_groups != client_groups {
                tracing::debug!("server groups mismatch");
                anyhow::bail!("Server groups do not match");
            }
        }
    }
    if let Some(server_nofile) = props.rlimit_nofile {
        if let Some(client_nofile) = util::rlimit_nofile() {
            if server_nofile < client_nofile {
                tracing::debug!("server RLIMIT_NOFILE incompatible");
                anyhow::bail!("Server RLIMIT_NOFILE is incompatible");
            }
        }
    }

    // Replace the server's env vars and chdir.
    // Disable demandimport as modules are expected to be pre-imported.
    let mut env = CommandEnv::current()?;
    env.env
        .push(("HGDEMANDIMPORT".to_owned(), "disable".to_owned()));
    let mask = util::get_umask();
    let applied = ServerIpc::apply_env(&client, env, mask)?;
    if !applied {
        tracing::debug!("server apply_env failed");
        anyhow::bail!("Server cannot apply env");
    }

    // Send the run_command request.
    // Note the server might ask the client for "ui.system" requests.
    tracing::debug!("sending command request");
    let ret = ServerIpc::run_command(&client, args.clone())?;
    tracing::debug!("command {:?} returned: {}", &args, ret);
    Ok(ret)
}

/// Check if a command should run remotely, with reasons.
/// See also `hgmain::chg`.
fn should_run_remotely(args: &[String]) -> (bool, &'static str) {
    // Bash might translate `<(...)` to `/dev/fd/x` instead of using a real fifo. That
    // path resolves to different fd by the chg server. Therefore chg cannot be used.
    if cfg!(unix)
        && args
            .iter()
            .any(|a| a.starts_with("/dev/fd/") || a.starts_with("/proc/self/"))
    {
        return (false, "arg starts with /dev/fd or /proc/self/");
    }

    // stdin is not a tty but stdout is a tty. Interactive pager is used
    // but lack of ctty makes it impossible to control the interactive
    // pager via keys.
    if cfg!(unix) && !std::io::stdin().is_terminal() && std::io::stdout().is_terminal() {
        return (false, "!stdin.is_tty() && stdout.is_tty()");
    }

    (true, "")
}