/*
  Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; version 2 of the License.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

#ifndef HARNESS_ARG_HANDLER_INCLUDED
#define HARNESS_ARG_HANDLER_INCLUDED

/** @file
 * @brief Defining the commandline argument handler class CmdArgHandler
 *
 * This file defines the commandline argument handler class CmdArgHandler.
 */

#include "harness_export.h"

#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>

enum class CmdOptionValueReq : uint8_t {
  none = 0x01,
  required = 0x02,
  optional = 0x03,
};

using ActionFunc = std::function<void(const std::string &)>;
using OptionNames = std::vector<std::string>;

/** @brief CmdOption stores information about command line options
 *
 * The CmdOption structure stores information about command line options.
 *
 */
struct CmdOption {
  using ActionFunc  = std::function<void(const std::string &)>;
  using OptionNames = std::vector<std::string>;

  OptionNames names;
  std::string description;
  CmdOptionValueReq value_req;
  std::string value;
  std::string metavar;
  ActionFunc action;

  CmdOption(OptionNames names_,
            std::string description_,
            CmdOptionValueReq value_req_,
            const std::string metavar_,
            ActionFunc action_)
  : names(names_), description(description_), value_req(value_req_),
    metavar(metavar_), action(action_) {}
};

/** @brief Definition of a vector holding unique pointers to CmdOption
 * objects **/
using OptionContainer = std::vector<CmdOption>;

/** @class CmdArgHandler
 *  @brief Handles command line arguments
 *
 * The CmdArgHandler class handles command line arguments. It is a
 * replacement and supports most of the POSIX GNU getopt library.
 *
 * Command line options can have multiple aliases. For example, the
 * typical `--help` can be also called `-h`, or even `--help-me`. Long
 * names starting with one dash are not supported.
 *
 * Command line options are added through the `add_option()` method
 * and can be given 1 or more names and a description. It is also
 * possible to require the option to have a value, or make the value
 * optional.
 *
 * During processing of the command line arguments, actions will be
 * executed when the option (and it's potential value) was found.
 *
 * Usage example:
 *
 *     #include "arg_handler.h"
 *
 *     void MyApp::prepare_command_line_options() {
 *       // CmdArgHandler handler_;
 *       handler_.add_option(OptionNames({"-h", "--help", "--sos"}), "Show help screen",
 *                          CmdOptionValueReq::none, "",
 *                          [this](const string &) { this->show_help(); }
 *       handler_.add_option(OptionNames({"--config"}), "Configuration file",
 *                          CmdOptionValueReq::none, "",
 *                          [this](const string &value) { this->set_config_file(value); }
 *     }
 *
 *     void MyApp::init(const vector<string> arguments) {
 *       prepare_command_line_options();
 *       handler_.process(arguments);
 *     }
 *
 *  All arguments which are not valid option names are are not values of options are considered
 *  rest arguments. By default, rest arguments are not allowed and will raise a std::invalid_argument
 *  exception. It is possible to allow them through the constructor.
 *
 *  The CmdArgHandler class also provides functionality to help creating help
 *  screen or show how to use the the command line application. The method `usage_lines()`
 *  produces a usage line showing all the option names, their required or optional
 *  value with a meta variable. Similar, method `option_descriptions()` will get
 *  all options and their descriptions. All this is text wrapped at a configurable
 *  margin as well as, if needed, indented.
 *
 *  @devnote
 *  The command line argument handling in CmdArgHandler is the bare minimum needed
 *  for MySQL Router. It was needed to make sure that the application would compile
 *  on system where the getopt library is not available.
 *  @enddevnote
 *
 */
class HARNESS_EXPORT CmdArgHandler {
 public:
  /** @brief Constructor
   *
   * @param allow_rest_arguments whether we allow rest arguments or not
   */
  explicit CmdArgHandler(bool allow_rest_arguments_)
      : allow_rest_arguments(allow_rest_arguments_) {}

  /** @brief Default constructor
   *
   * By default, rest arguments are not allowed.
   */
  CmdArgHandler() : CmdArgHandler(false) {}

  /** @brief Adds a command line option
   *
   * Adds a command line option given names, description, value
   * requirement and optional action.
   *
   * `names` is a vector of strings which contains names starting
   * with either a single or double dash, `-` or `--`. It is possible
   * to add more than one name for an option.
   *
   * The description text will be used in the help output. Note that
   * this can be a very long as text will be wrapped (and optionally
   * indented). New lines in the description will be respected.
   *
   * The `metavar` argument is used in the usage text as a placeholder
   * for the (optional) value of the option, for example, when `metavar`
   * is set to `path`, the usage would show:
   *
   *     --config=<path>
   *
   * The value_req argument should be either:
   *
   * * `CmdOptionValueReq::none` : option has no value
   * * `CmdOptionValueReq::required` : option requires value
   * * `CmdOptionValueReq::optional` : option has optional value
   *
   * The `action` argument should be a `std::function` and is called
   * with the (optional) value of the option. The function should
   * accept only a `const std::string`.
   *
   * Example usage:
   *
   *       handler_.add_option(OptionNames({"--config"}), "Configuration file",
   *                          CmdOptionValueReq::none, "",
   *                          [this](const string &value) { this->set_config_file(value); }
   *
   * @devnote
   * The `add_option` method will assert when `names` is empty,
   * one of the names is not valid or when a name was already used with
   * another option.
   * @enddevnote
   *
   * @param names vector of string with option names, each starting with - or --
   * @param description descriptive text explaining the option
   * @param value_req value requirement of the option
   * @param metavar for formatting help text when option accepts a value
   * @param action action to perform when the option was found
   */
  void add_option(const OptionNames& names,
                  const std::string& description,
                  const CmdOptionValueReq& value_req,
                  const std::string& metavar,
                  ActionFunc action) noexcept;

  void add_option(const CmdOption &other) noexcept;

  /** @brief Processes given command line arguments
   *
   * Processes given command line argument provided as a vector
   * of strings. It uses the stored option information added through the
   * `add_option()` method.
   *
   * Typically, the vector passed to process() are the argc and argv
   * arguments of the main() function:
   *
   *     process({argv + 1, argv + argc})
   *
   * When an option is found which requires an argument, optional or
   * not, process() will exit the application with an error message.
   *
   * If the option has an action defined, the function will be
   * executed with the (optional) value as argument.
   *
   * @param arguments vector of strings
   */
  void process(const std::vector<std::string>& arguments);

#ifndef NDEBUG
  bool debug_check_option_names(const CmdOption::OptionNames& names) const;
#endif

  /** @brief Checks whether given name is a valid option name
   *
   * Checks whether the given name is a valid option name.
   *
   * A valid option name should:
   *
   * * have at least consist of 2 characters
   * * start with a dash '-'
   * * match the reqular expression ^--[A-Za-z]{2}[A-Za-z_-]+$
   *
   * It is allowed to use the equal sign when giving value. Following options
   * are equal:
   *     --config /path/to/mysqlrouter.conf
   *     --config=/path/to/mysqlrouter.conf
   *
   * Throws std::invalid_argument when option name is not valid or
   * option was not registered.
   *
   * Examples of valid option names:
   *
   *     -h
   *     --with-ham
   *     --with_spam
   *
   * Example of invalid option names:
   *
   *     -help
   *     ---spam
   *     --x-ham
   *
   * @param name option name to check
   * @return true if name is valid; false otherwise
   */
  bool is_valid_option_name(const std::string& name) const noexcept;

  /** @brief Finds the option by name
   *
   * Finds the option by one of its name. The name should include the the dash prefix.
   *
   * Example usage:
   *     // check if option name is already present
   *     assert(options_.end() == find_option(name))
   *
   * @param name name of the option as string
   * @returns iterator object
   */
  OptionContainer::const_iterator
      find_option(const std::string& name) const noexcept;

  /** @brief Produces lines of text suitable to show usage
   *
   * Produces lines of text suitable to show usage of the command line
   * appliation. Each option is shown with all its names and with optional
   * or required value.
   *
   * The `prefix` argument can be used to add text, usually the name
   * of the command, in front of the options. The lines are indented
   * using the size of the prefix.
   *
   * The `rest_metavar` can be used to name non-options arguments.
   *
   * The `width` argument is used to set the maximum length of the lines.
   *
   * Example output when all lines are printed:
   *
   *     usage: mysqlrouter [-v|--version] [-h|--help] [-c|--config=<path>]
   *                        [-a=[<foo>]] [rest..]
   *
   *
   * @param prefix text in front of options (usually command name)
   * @param rest_metavar name of rest arguments (empty if not needed)
   * @param width maximum length of each line
   * @return vector of strings
   */
  std::vector<std::string>
      usage_lines(const std::string& prefix,
                  const std::string& rest_metavar,
                  size_t width) const noexcept;

  /** @brief Produces description of all options
   *
   * Produces description of all options. The result is typically shown
   * when the help screen is requested, for example when the `--help`
   * option is given.
   *
   * The `width` argument is used to set the maximum length of the lines. Text is
   * wrapped accordingly.
   *
   * Each description can be indented using space. The amount is given
   * by `indent` option.
   *
   * Example output when lines are printed:
   *
   *     -v, --version
   *           Show version
   *     -h, --help
   *           Show help
   *     -c <path>, --config <path>
   *           Path to the configuration file
   *
   * @param width maximum length of each line
   * @param indent how much the description should be indented.
   * @return vector of strings
   */
  std::vector<std::string> option_descriptions(const size_t width,
                                               const size_t indent) noexcept;

  /** @brief Returns an iterator to first option
   *
   * Returns an iterator to the first option.
   *
   * @returns iterator
   */
  OptionContainer::const_iterator begin() {
    return options_.begin();
  }

  /** @brief Returns an iterator to end of the option container
   *
   * Returns an iterator to the end of the option container.
   *
   * @returns iterator
   */
  OptionContainer::const_iterator end() {
    return options_.end();
  }

  /** @brief Clears registered options
   *
   * Clears the registered options.
   *
   */
  void clear_options() {
    options_.clear();
  }

  /** @brief Gets all registered options
   *
   * Returns as a reference to a vector of CmdOption objects.
   *
   * @return std::vector<CmdOption>
   */
  const std::vector<CmdOption>& get_options() const noexcept {
    return options_;
  }

  /** @brief Returns the rest arguments
   *
   * Returns the rest arguments.
   *
   * If rest arguments are not allow or there were no rest arguments,
   * an empty vector is returned.
   *
   * @return vector of strings
   */
  const std::vector<std::string>& get_rest_arguments() const noexcept {
    return rest_arguments_;
  }

  /** @brief Whether to allow rest arguments or not **/
  bool allow_rest_arguments;

 private:
  /** @brief Vector with registered options **/
  std::vector<CmdOption> options_;
  /** @brief Vector with arguments as strings not processed as options **/
  std::vector<std::string> rest_arguments_;
};

#endif // HARNESS_ARG_HANDLER_INCLUDED
