/** @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();

  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
  };

  // Class for commands in a nested way
  class Command
  {
  public:
    // Constructor and destructor
    Command();
    ~Command();
    /** Add an option to current command
        @return The Option object.
    */
    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 = "");

    /** 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;
    // Helpr method for parse()
    void append_option_data(Arguments &ret, AP_StrVec &args, int index);
    // 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;
    // An example usage can be added for the help message
    std::string _example_usage;
    // 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;

    // 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 Option object.
  */
  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 = "");

  /** 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 defaut 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;

protected:
  // 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
