// TODO: 
//  * support Arg::conflicts_with(name) (implement use user provider parser closure)
//  * support Arg::required_if/required_unless (implement use user provider parser closure)
//  * add more error tests
//  * add more docs

///|
pub(all) struct Parser {
  /// The program name
  prog : String
  args : Map[String, Arg]
  /// If there is sub-commands position, positional arguments in currently level is forbiden.
  subcmds : Map[String, SubCommand]
  description : String
}

///|
/// Create the argument parser specification
pub fn Parser::new(
  prog? : String = "PROG",
  args? : Map[String, Arg] = {},
  subcmds? : Map[String, SubCommand] = {},
  description? : String = "",
) -> Parser {
  { prog, args, subcmds, description }
}

///|
pub(all) suberror ParserError {
  InvalidArgumentName(String)
  InvalidSubCommandName(String)
  InvalidPositionalAsNamed(String)
  InvalidArgumentValue(String)
  InvalidArgumentValueLength(String)
  TooManyArgs(String)
  InvalidSpec(String)
} derive(Show)

///|
/// Parse the command line arguments
pub fn[V : Value] Parser::parse(
  self : Parser,
  value : V,
  cli_args : ArrayView[String],
  env_vars? : Map[String, String] = {},
) -> String? raise ParserError {
  let mut cmd_name = self.prog
  let subcmd_chains = [cmd_name]
  let global_args : Map[String, Arg] = {}
  let mut args = self.args
  let mut positional_arg : (String, Arg)? = None
  let mut subcmds = self.subcmds
  let args_length : @hashmap.HashMap[String, UInt] = @hashmap.new()
  let mut current_value = value
  let mut current_arg : (String, Arg)? = None
  let mut nargs_error : ParserError? = None
  if analysis_and_check_spec(cmd_name, args, global_args, subcmds, env_vars)
    is Some(rv) {
    positional_arg = Some(rv)
  }
  for cli_arg in cli_args {
    match cli_arg {
      [.. "--", .. name] => {
        if name == "help" {
          return Some(self.gen_help_message(subcmd_chains, global_args))
        }
        let name = name.to_string()
        let new_arg = args
          .get(name)
          .map_or_else(fn() { global_args.get(name) }, fn(x) { Some(x) })
          .map(fn(arg) { (name, arg) })
        if new_arg.is_empty() && positional_arg is Some(arg) {
          current_value.add_value(arg.0, cli_arg, true)
          update_args_length(args_length, arg.0)
        } else {
          current_arg = handle_current_arg(
            current_value, cli_arg, new_arg, cmd_name, args_length,
          )
        }
      }
      [.. "-", .. rest] =>
        if rest.char_length() != 1 {
          if positional_arg is Some(arg) {
            current_value.add_value(arg.0, cli_arg, true)
            update_args_length(args_length, arg.0)
          } else {
            raise InvalidArgumentName(
              "Invalid short argument name \{cli_arg} for \{cmd_name}",
            )
          }
        } else {
          let short_char = rest.get_char(0).unwrap()
          if short_char == 'h' {
            return Some(self.gen_help_message(subcmd_chains, global_args))
          }
          let new_arg = args
            .iter()
            .find_first(fn(pair) { pair.1.short() == Some(short_char) })
            .map_or_else(
              fn() {
                global_args
                .iter()
                .find_first(fn(pair) { pair.1.short() == Some(short_char) })
              },
              fn(x) { Some(x) },
            )
          if new_arg.is_empty() && positional_arg is Some(arg) {
            current_value.add_value(arg.0, cli_arg, true)
            update_args_length(args_length, arg.0)
          } else {
            current_arg = handle_current_arg(
              current_value, cli_arg, new_arg, cmd_name, args_length,
            )
          }
        }
      // == Normal argument ==
      value_string if current_arg is Some((name, arg)) => {
        if arg.choices() is Some(choices) {
          if choices.length() > 0 && not(choices.contains(value_string)) {
            raise InvalidArgumentValue(
              "Invalid argument value for \{cmd_name} => \{name}, value=\{value_string}, choices=\{choices}",
            )
          }
        }
        current_value.add_value(name, value_string, false)
        update_args_length(args_length, name)
        let new_length = args_length.get_or_default(name, 0)
        let nargs = arg.nargs().unwrap()
        if nargs.is_exceeded(new_length) {
          raise TooManyArgs(
            "Argument length limit: \{nargs}, current length: \{new_length}",
          )
        }
        if (not(positional_arg.is_empty()) || not(subcmds.is_empty())) &&
          nargs.is_exceeded(new_length + 1) {
          // Stop accepting more named arguments if nargs is full filled
          current_arg = None
        }
      }
      // == SubCommand ==
      value if subcmds.get(value) is Some(subcmd) => {
        if complete_level(
            args, global_args, args_length, current_value, env_vars, false,
          )
          is Some(err) {
          nargs_error = Some(err)
        }
        // switch to selected sub-command
        cmd_name = value.to_string()
        subcmd_chains.push(cmd_name)
        args = subcmd.args
        subcmds = subcmd.subcmds
        args_length.clear()
        current_value = current_value.select_subcmd(cmd_name)
        current_arg = None
        if analysis_and_check_spec(
            cmd_name, args, global_args, subcmds, env_vars,
          )
          is Some(rv) {
          positional_arg = Some(rv)
        }
      }
      // == Positional argument ==
      value_string if positional_arg is Some(arg) => {
        let name = arg.0
        current_value.add_value(name, value_string, true)
        update_args_length(args_length, name)
      }
      value =>
        raise InvalidSubCommandName(
          "Invalid sub-command name \{value} for \{cmd_name}",
        )
    }
  }
  if complete_level(
      args, global_args, args_length, current_value, env_vars, true,
    )
    is Some(err) {
    nargs_error = Some(err)
  }
  if nargs_error is Some(err) {
    raise err
  }
  None
}

///|
fn get_defaults(arg : Arg, env_vars : Map[String, String]) -> Iter[StringView] {
  let common = arg.common()
  if not(common.env_var.is_empty()) &&
    env_vars.get(common.env_var) is Some(env_value) {
    let value_delimiter = arg.value_delimiter().unwrap()
    env_value.split(value_delimiter)
  } else {
    arg.defaults().iter().map(fn(s) { s.view() })
  }
}

///|
fn update_args_length(
  args_length : @hashmap.HashMap[String, UInt],
  name : String,
) -> Unit {
  args_length.set(name, args_length.get_or_default(name, 0) + 1)
}

///|
fn[V : Value] handle_current_arg(
  current_value : V,
  cli_arg : String,
  new_arg : (String, Arg)?,
  cmd_name : String,
  args_length : @hashmap.HashMap[String, UInt],
) -> (String, Arg)? raise ParserError {
  guard new_arg is Some((name, arg)) else {
    raise InvalidArgumentName(
      "Invalid argument name \{cli_arg} for \{cmd_name}",
    )
  }
  if arg is Positional(..) {
    raise InvalidPositionalAsNamed(
      "Invalid use positional argument as name argument: \{cli_arg} for \{cmd_name} ",
    )
  }
  if arg is Flag(store~, ..) {
    current_value.set_flag(name, store)
    update_args_length(args_length, name)
    // Only one flag can be in a single level, so reset current_arg
    None
  } else {
    Some((name, arg))
  }
}

///|
fn analysis_and_check_spec(
  cmd_name : String,
  args : Map[String, Arg],
  global_args : Map[String, Arg],
  subcmds : Map[String, SubCommand],
  env_vars : Map[String, String],
) -> (String, Arg)? raise ParserError {
  let mut has_positional = false
  let mut positional_arg = None
  for name, arg in args.iter2() {
    let positional = arg is Positional(_)
    if positional {
      if not(subcmds.is_empty()) {
        raise InvalidSpec(
          "Invalid argument spec: sub-commands(parent=\{cmd_name}) and positional argument(\{name}) are not allowed at the same time",
        )
      } else {
        positional_arg = Some((name, arg))
      }
    }
    if positional && has_positional {
      raise InvalidSpec(
        "Invalid argument, only one positional argument is allowed, second=\{name}",
      )
    }
    has_positional = has_positional || positional
    if arg.is_global() {
      global_args[name] = arg
    }
    if arg.short() is Some(short_char) {
      if short_char != null_short &&
        not(short_char.is_ascii_alphabetic()) &&
        not(short_char.is_ascii_digit()) {
        raise InvalidSpec(
          "Invalid argument spec: parent-cmd=\{cmd_name}, name=\{name}, short=\{short_char}, only 0-9,a-z,A-Z is allowed",
        )
      }
    }
    if arg.choices() is Some(choices) && not(choices.is_empty()) {
      for value in get_defaults(arg, env_vars) {
        if not(choices.contains(value.to_string())) {
          raise InvalidSpec(
            "Invalid argument, name=\{name}, default value=\{value} not in choices=\{choices}",
          )
        }
      }
    }
  }
  positional_arg
}

// * set default value if missing
// * check argument value length is valid

///|
fn[V : Value] complete_level(
  args : Map[String, Arg],
  global_args : Map[String, Arg],
  args_length : @hashmap.HashMap[String, UInt],
  current_value : V,
  env_vars : Map[String, String],
  final_subcmd : Bool,
) -> ParserError? raise ParserError {
  let mut nargs_error : ParserError? = None
  let all_args = if final_subcmd {
    [(args, false), (global_args, true)]
  } else {
    [(args, false)]
  }
  for item in all_args {
    let (the_args, is_global) = item
    for name, arg in the_args.iter2() {
      if is_global && args.contains(name) {
        continue
      }
      let positional = arg is Positional(..)
      let mut length = args_length.get_or_default(name, 0)
      // flag and named argument can only presented in final subcommand
      if (arg is Flag(_) || arg is Named(_)) && length > 0 && not(final_subcmd) {
        raise InvalidArgumentValueLength(
          "Invalid argument: \{name}, flag/named argument can only presented in final level",
        )
      }
      if arg is Flag(store~, ..) {
        if length == 0 {
          let common = arg.common()
          let value = if not(common.env_var.is_empty()) &&
            env_vars.get(common.env_var) is Some(env_value) {
            match env_value {
              "true" => true
              "false" => false
              _ =>
                raise InvalidArgumentValue(
                  "Invalid flag environment variable: \{common.env_var}=\{env_value}",
                )
            }
          } else {
            not(store)
          }
          current_value.set_flag(name, value)
          update_args_length(args_length, name)
        } else if length > 1 {
          // TODO: support multiple flag in the future?
          raise InvalidArgumentValueLength(
            "Invalid flag argument length: \{length} > 1",
          )
        }
      } else {
        // Set defaults for:
        //   * final level positional argument
        //   * non-final level named argument
        if length == 0 && not(positional && not(final_subcmd)) {
          for default_value in get_defaults(arg, env_vars) {
            current_value.add_value(name, default_value.to_string(), positional)
            update_args_length(args_length, name)
            length += 1
          }
        }
        let nargs = arg.nargs().unwrap()
        if not(nargs.is_valid_length(length)) {
          if positional {
            if not(final_subcmd) {
              if length != 0 {
                raise InvalidArgumentValueLength(
                  "Invalid positional argument length=\{length}, expected: 0 (has subcommand)",
                )
              }
            } else {
              nargs_error = Some(
                InvalidArgumentValueLength(
                  "Invalid positional argument length=\{length}, limit: \{nargs}",
                ),
              )
            }
          } else if final_subcmd {
            nargs_error = Some(
              InvalidArgumentValueLength(
                "Invalid name argument length=\{length}, limit: \{nargs}, final: \{final_subcmd}",
              ),
            )
          }
        }
      }
    }
  }
  nargs_error
}