/** @file

  Powerful and easy-to-use command line parsing for ATS

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
 */

#pragma once

#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <functional>
#include <string_view>

// more than zero arguments
constexpr unsigned MORE_THAN_ZERO_ARG_N = ~0;
// more than one arguments
constexpr unsigned MORE_THAN_ONE_ARG_N = ~0 - 1;
// customizable indent for help message
constexpr int INDENT_ONE = 32;
constexpr int INDENT_TWO = 46;

namespace ts
{
using AP_StrVec = std::vector<std::string>;
// The class holding both the ENV and String arguments
class ArgumentData
{
public:
  // bool to check if certain command/option is called
  operator bool() const noexcept { return _is_called; }
  // index accessing []
  std::string const &
  operator[](int x) const
  {
    return _values.at(x);
  }
  // return the Environment variable
  std::string const &env() const noexcept;
  // iterator for arguments
  AP_StrVec::const_iterator begin() const noexcept;
  AP_StrVec::const_iterator end() const noexcept;
  // index accessing
  std::string const &at(unsigned index) const;
  // access the first index, equivalent to at(0)
  std::string const &value() const noexcept;
  // size of _values
  size_t size() const noexcept;
  // return true if _values and _env_value are both empty
  bool empty() const noexcept;

private:
  bool _is_called = false;
  // the environment variable
  std::string _env_value;
  // the arguments stored
  AP_StrVec _values;

  friend class Arguments;
};

// The class holding all the parsed data after ArgParser::parse()
class Arguments
{
public:
  Arguments();
  Arguments(const Arguments &)            = default;
  Arguments(Arguments &&)                 = default;
  Arguments &operator=(const Arguments &) = default;
  Arguments &operator=(Arguments &&)      = default;
  ~Arguments();

  ArgumentData get(std::string const &name);

  void append(std::string const &key, ArgumentData const &value);
  // Append value to the arg to the map of key
  void append_arg(std::string const &key, std::string const &value);
  // append env value to the map with key
  void set_env(std::string const &key, std::string const &value);
  // Print all we have in the parsed data to the console
  void show_all_configuration() const;
  /** Invoke the function associated with the parsed command.
      @return The return value of the executed command (int).
  */
  void invoke();
  // return true if there is any function to invoke
  bool has_action() const;

private:
  // A map of all the called parsed args/data
  // Key: "command/option", value: ENV and args
  std::map<std::string, ArgumentData> _data_map;
  // The function associated. invoke() will call this func
  std::function<void()> _action;

  friend class ArgParser;
  friend class ArgumentData;
};

// Class of the ArgParser
class ArgParser
{
  using Function = std::function<void()>;

public:
  // Option structure: e.x. --arg -a
  // Contains all information about certain option(--switch)
  struct Option {
    std::string long_option;   // long option: --arg
    std::string short_option;  // short option: -a
    std::string description;   // help description
    std::string envvar;        // stored ENV variable
    unsigned    arg_num;       // number of argument expected
    std::string default_value; // default value of option
    std::string key;           // look-up key
  };

  // Mutually exclusive group structure
  // Options in the same group cannot be used together
  struct MutexGroup {
    std::string              name;            // group identifier
    std::vector<std::string> options;         // list of long option names in this group
    bool                     required{false}; // if true, one option from group must be specified
    std::string              description;     // optional description for help message

    MutexGroup(std::string const &n, bool req = false, std::string const &desc = "") : name(n), required(req), description(desc) {}
  };

  // Class for commands in a nested way
  class Command
  {
  public:
    // Constructor and destructor
    Command();
    Command(const Command &)            = default;
    Command(Command &&)                 = default;
    Command &operator=(const Command &) = default;
    Command &operator=(Command &&)      = default;
    ~Command();
    /** Add an option to current command
        @return The Command object for chaining.
    */
    Command &add_option(std::string const &long_option, std::string const &short_option, std::string const &description,
                        std::string const &envvar = "", unsigned arg_num = 0, std::string const &default_value = "",
                        std::string const &key = "");

    /** Create a mutually exclusive group of options
        @param group_name Identifier for the group
        @return The Command object for chaining.
    */
    Command &add_mutex_group(std::string const &group_name, bool required = false, std::string const &description = "");

    /** Add an option to a mutually exclusive group
        @param group_name The group to add the option to
        @return The Command object for chaining.
    */
    Command &add_option_to_group(std::string const &group_name, std::string const &long_option, std::string const &short_option,
                                 std::string const &description, std::string const &envvar = "", unsigned arg_num = 0,
                                 std::string const &default_value = "", std::string const &key = "");

    /** Specify that the last added option requires another option to be present.
        Must be called immediately after add_option() or add_option_to_group().
        @param required_option The option that must be present (e.g., "--tags")
        @return The Command instance for chained calls.
    */
    Command &with_required(std::string const &required_option);

    /** Two ways of adding a sub-command to current command:
        @return The new sub-command instance.
    */
    Command &add_command(std::string const &cmd_name, std::string const &cmd_description, Function const &f = nullptr,
                         std::string const &key = "");
    Command &add_command(std::string const &cmd_name, std::string const &cmd_description, std::string const &cmd_envvar,
                         unsigned cmd_arg_num, Function const &f = nullptr, std::string const &key = "");
    /** Add an example usage of current command for the help message
        @return The Command instance for chained calls.
    */
    Command &add_example_usage(std::string const &usage);
    /** Require subcommand/options for this command
        @return The Command instance for chained calls.
    */
    Command &require_commands();
    /** set the current command as default
        @return The Command instance for chained calls.
    */
    Command &set_default();

  protected:
    // Main constructor called by add_command()
    Command(std::string const &name, std::string const &description, std::string const &envvar, unsigned arg_num, Function const &f,
            std::string const &key = "");
    // Helper method for add_option to check the validity of option
    void check_option(std::string const &long_option, std::string const &short_option, std::string const &key) const;
    // Helper method for add_command to check the validity of command
    void check_command(std::string const &name, std::string const &key) const;
    // Helper method for ArgParser::help_message
    void output_command(std::ostream &out, std::string const &prefix) const;
    // Helper method for ArgParser::help_message
    void output_option() const;
    // Helper method for ArgParser::parse
    bool parse(Arguments &ret, AP_StrVec &args);
    // The help & version messages
    void help_message(std::string_view err = "") const;
    void version_message() const;
    // Helper method for parse()
    void append_option_data(Arguments &ret, AP_StrVec &args, int index);
    // Helper method to validate mutually exclusive groups
    void validate_mutex_groups(Arguments &ret) const;
    // Helper method to validate option dependencies
    void validate_dependencies(Arguments &ret) const;
    // Helper method to apply default values for options not explicitly set
    void apply_option_defaults(Arguments &ret) const;
    // The command name and help message
    std::string _name;
    std::string _description;

    // Expected argument number
    unsigned _arg_num = 0;
    // Stored Env variable
    std::string _envvar;
    // Example usages can be added for the help message
    std::vector<std::string> _example_usages;
    // Function associated with this command
    Function _f;
    // look up key
    std::string _key;

    // list of all subcommands of current command
    // Key: command name. Value: Command object
    std::map<std::string, Command> _subcommand_list;
    // list of all options of current command
    // Key: option name. Value: Option object
    std::map<std::string, Option> _option_list;
    // Map for fast searching: <short option: long option>
    std::map<std::string, std::string> _option_map;

    // Mutually exclusive groups
    // Key: group name. Value: MutexGroup object
    std::map<std::string, MutexGroup> _mutex_groups;
    // Map option to its mutex group (for fast lookup during validation)
    // Key: long option name. Value: group name
    std::map<std::string, std::string> _option_to_group;

    // Option dependencies: dependent_option -> list of required options
    std::map<std::string, std::vector<std::string>> _option_dependencies;
    // Track the last added option for with_required() chaining
    std::string _last_added_option;

    // require command / option for this parser
    bool _command_required = false;

    friend class ArgParser;
  };
  // Base class constructors and destructor
  ArgParser();
  ArgParser(std::string const &name, std::string const &description, std::string const &envvar, unsigned arg_num,
            Function const &f);
  ~ArgParser();

  /** Add an option to current command with arguments
      @return The Command object for chaining.
  */
  Command &add_option(std::string const &long_option, std::string const &short_option, std::string const &description,
                      std::string const &envvar = "", unsigned arg_num = 0, std::string const &default_value = "",
                      std::string const &key = "");

  /** Create a mutually exclusive group of options
      @param group_name Identifier for the group
      @return The Command object for chaining.
  */
  Command &add_mutex_group(std::string const &group_name, bool required = false, std::string const &description = "");

  /** Add an option to a mutually exclusive group
      @param group_name The group to add the option to
      @return The Command object for chaining.
  */
  Command &add_option_to_group(std::string const &group_name, std::string const &long_option, std::string const &short_option,
                               std::string const &description, std::string const &envvar = "", unsigned arg_num = 0,
                               std::string const &default_value = "", std::string const &key = "");

  /** Two ways of adding command to the parser:
      @return The new command instance.
  */
  Command &add_command(std::string const &cmd_name, std::string const &cmd_description, Function const &f = nullptr,
                       std::string const &key = "");
  Command &add_command(std::string const &cmd_name, std::string const &cmd_description, std::string const &cmd_envvar,
                       unsigned cmd_arg_num, Function const &f = nullptr, std::string const &key = "");
  // give a default command to this parser
  void set_default_command(std::string const &cmd);
  /** Main parsing function
      @return The Arguments object available for program using
  */
  Arguments parse(const char **argv);
  // Add the usage to global_usage for help_message(). Something like: traffic_blabla [--SWITCH [ARG]]
  void add_global_usage(std::string const &usage);
  // help message that can be called
  void help_message(std::string_view err = "") const;
  /** Require subcommand/options for this command
      @return The Command instance for chained calls.
  */
  Command &require_commands();
  // set the error message
  void set_error(std::string e);
  // get the error message
  std::string get_error() const;

  // Add App's description.
  void add_description(std::string const &descr);

protected:
  // Exit handler - throws in test mode, calls exit() otherwise
  static void do_exit(int code);
  // Enable test mode - makes do_exit() throw instead of calling exit() for unit testing
  static void set_test_mode(bool test = true);

  // When true, do_exit() throws instead of calling exit()
  static bool _test_mode;

  // Converted from 'const char **argv' for the use of parsing and help
  AP_StrVec _argv;
  // the top level command object for program use
  Command _top_level_command;
  // user-customized error message output
  std::string _error_msg;

  friend class Command;
  friend class Arguments;
};

} // namespace ts
