/***
     ____   __  ____  __
    (  _ \ /  \(  _ \(  )
     ) __/(  O )) __// (_/\
    (__)   \__/(__)  \____/
    version 1.3.1
    https://github.com/badaix/popl

    This file is part of popl (program options parser lib)
    Copyright (C) 2015-2021 Johannes Pohl

    This software may be modified and distributed under the terms
    of the MIT license.  See the LICENSE file for details.
***/

/// checked with clang-tidy:
/// run-clang-tidy-3.8.py -header-filter='.*'
/// -checks='*,-misc-definitions-in-headers,-google-readability-braces-around-statements,-readability-braces-around-statements,-cppcoreguidelines-pro-bounds-pointer-arithmetic,-google-build-using-namespace,-google-build-using-namespace'

#ifndef POPL_HPP
#define POPL_HPP

#ifndef NOMINMAX
#define NOMINMAX
#endif // NOMINMAX

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iostream>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <vector>
#ifdef WINDOWS
#include <cctype>
#endif


namespace popl
{

#define POPL_VERSION "1.3.0"


/// Option's argument type
/**
 * Switch has "no" argument
 * Value has "required" argument
 * Implicit has "optional" argument
 */
enum class Argument
{
    no = 0,   // option never takes an argument
    required, // option always requires an argument
    optional  // option may take an argument
};


/// Option's attribute
/**
 * inactive: Option is not set and will not be parsed
 * hidden:   Option is active, but will not show up in the help message
 * required: Option must be set on the command line. Otherwise an exception will be thrown
 * optional: Option must not be set. Default attribute.
 * advanced: Option is advanced and will only show up in the advanced help message
 * expoert:  Option is expert and will only show up in the expert help message
 */
enum class Attribute
{
    inactive = 0,
    hidden = 1,
    required = 2,
    optional = 3,
    advanced = 4,
    expert = 5
};


/// Option name type. Used in invalid_option exception.
/**
 * unspecified: not specified
 * short_name:  The option's short name
 * long_name:   The option's long name
 */
enum class OptionName
{
    unspecified,
    short_name,
    long_name
};


/// Abstract Base class for Options
/**
 * Base class for Options
 * holds just configuration data, no runtime data.
 * Option is not bound to a special type "T"
 */
class Option
{
    friend class OptionParser;

public:
    /// Construct an Option
    /// @param short_name the options's short name. Must be empty or one character.
    /// @param long_name the option's long name. Can be empty.
    /// @param description the Option's description that will be shown in the help message
    Option(const std::string& short_name, const std::string& long_name, std::string description);

    /// Destructor
    virtual ~Option() = default;

    /// default copy constructor
    Option(const Option&) = default;

    /// default move constructor
    Option(Option&&) = default;

    /// default assignement operator
    Option& operator=(const Option&) = default;

    /// default move assignement operator
    Option& operator=(Option&&) = default;

    /// Get the Option's short name
    /// @return character of the options's short name or 0 if no short name is defined
    char short_name() const;

    /// Get the Option's long name
    /// @return the long name of the Option. Empty string if no long name is defined
    std::string long_name() const;

    /// Get the Option's long or short name
    /// @param what_name the option's name to return
    /// @param what_hyphen preced the returned name with (double-)hypen
    /// @return the requested name of the Option. Empty string if not defined.
    std::string name(OptionName what_name, bool with_hypen = false) const;

    /// Get the Option's description
    /// @return the description
    std::string description() const;

    /// Get the Option's default value
    /// @param out stream to write the default value to
    /// @return true if a default value is available, false if not
    virtual bool get_default(std::ostream& out) const = 0;

    /// Set the Option's attribute
    /// @param attribute
    void set_attribute(const Attribute& attribute);

    /// Get the Option's attribute
    /// @return the Options's attribute
    Attribute attribute() const;

    /// Get the Option's argument type
    /// @return argument type (no, required, optional)
    virtual Argument argument_type() const = 0;

    /// Check how often the Option is set on command line
    /// @return the Option's count on command line
    virtual size_t count() const = 0;

    /// Check if the Option is set
    /// @return true if set at least once
    virtual bool is_set() const = 0;


protected:
    /// Parse the command line option and fill the internal data structure
    /// @param what_name short or long option name
    /// @param value the value as given on command line
    virtual void parse(OptionName what_name, const char* value) = 0;

    /// Clear the internal data structure
    virtual void clear() = 0;

    std::string short_name_;
    std::string long_name_;
    std::string description_;
    Attribute attribute_;
};



/// Value option with optional default value
/**
 * Value option with optional default value
 * If set, it requires an argument
 */
template <class T>
class Value : public Option
{
public:
    /// Construct an Value Option
    /// @param short_name the option's short name. Must be empty or one character.
    /// @param long_name the option's long name. Can be empty.
    /// @param description the Option's description that will be shown in the help message
    Value(const std::string& short_name, const std::string& long_name, const std::string& description);

    /// Construct an Value Option
    /// @param short_name the option's short name. Must be empty or one character.
    /// @param long_name the option's long name. Can be empty.
    /// @param description the Option's description that will be shown in the help message
    /// @param default_val the Option's default value
    /// @param assign_to pointer to a variable to assign the parsed command line value to
    Value(const std::string& short_name, const std::string& long_name, const std::string& description, const T& default_val, T* assign_to = nullptr);

    size_t count() const override;
    bool is_set() const override;

    /// Assign the last parsed command line value to "var"
    /// @param var pointer to the variable where is value is written to
    void assign_to(T* var);

    /// Manually set the Option's value. Deletes current value(s)
    /// @param value the new value of the option
    void set_value(const T& value);

    /// Get the Option's value. Will throw if option at index idx is not available
    /// @param idx the zero based index of the value (if set multiple times)
    /// @return the Option's value at index "idx"
    T value(size_t idx = 0) const;

    /// Get the Option's value, return default_value if not set.
    /// @param default_value return value if value is not set
    /// @param idx the zero based index of the value (if set multiple times)
    /// @return the Option's value at index "idx" or the default value or default_value
    T value_or(const T& default_value, size_t idx = 0) const;

    /// Set the Option's default value
    /// @param value the default value if not specified on command line
    void set_default(const T& value);

    /// Check if the Option has a default value
    /// @return true if the Option has a default value
    bool has_default() const;

    /// Get the Option's default value. Will throw if no default is set.
    /// @return the Option's default value
    T get_default() const;
    bool get_default(std::ostream& out) const override;

    Argument argument_type() const override;

protected:
    void parse(OptionName what_name, const char* value) override;
    std::unique_ptr<T> default_;

    virtual void update_reference();
    virtual void add_value(const T& value);
    void clear() override;

    T* assign_to_;
    std::vector<T> values_;
};



/// Value option with implicit default value
/**
 * Value option with implicit default value
 * If set, an argument is optional
 * -without argument it carries the implicit default value
 * -with argument it carries the explicit value
 */
template <class T>
class Implicit : public Value<T>
{
public:
    Implicit(const std::string& short_name, const std::string& long_name, const std::string& description, const T& implicit_val, T* assign_to = nullptr);

    Argument argument_type() const override;

protected:
    void parse(OptionName what_name, const char* value) override;
};



/// Value option without value
/**
 * Value option without value
 * Does not require an argument
 * Can be either set or not set
 */
class Switch : public Value<bool>
{
public:
    Switch(const std::string& short_name, const std::string& long_name, const std::string& description, bool* assign_to = nullptr);

    void set_default(const bool& value) = delete;
    Argument argument_type() const override;

protected:
    void parse(OptionName what_name, const char* value) override;
};



using Option_ptr = std::shared_ptr<Option>;

/// OptionParser manages all Options
/**
 * OptionParser manages all Options
 * Add Options (Option_Type = Value<T>, Implicit<T> or Switch) with "add<Option_Type>(option params)"
 * Call "parse(argc, argv)" to trigger parsing of the options and to
 * fill "non_option_args" and "unknown_options"
 */
class OptionParser
{
public:
    /// Construct the OptionParser
    /// @param description used for the help message
    explicit OptionParser(std::string description = "");

    /// Destructor
    virtual ~OptionParser() = default;

    /// Add an Option e.g. 'add<Value<int>>("i", "int", "description for the -i option")'
    /// @param T the option type (Value, Switch, Implicit)
    /// @param attribute the Option's attribute (inactive, hidden, required, optional, ...)
    /// @param Ts the Option's parameter
    template <typename T, Attribute attribute, typename... Ts>
    std::shared_ptr<T> add(Ts&&... params);

    /// Add an Option e.g. 'add<Value<int>>("i", "int", "description for the -i option")'
    /// @param T the option type (Value, Switch, Implicit)
    /// @param Ts the Option's parameter
    template <typename T, typename... Ts>
    std::shared_ptr<T> add(Ts&&... params);

    /// Parse an ini file into the added Options
    /// @param ini_filename full path of the ini file
    void parse(const std::string& ini_filename);

    /// Parse the command line into the added Options
    /// @param argc command line argument count
    /// @param argv command line arguments
    void parse(int argc, const char* const argv[]);

    /// Delete all parsed options
    void reset();

    /// Produce a help message
    /// @param max_attribute show options up to this level (optional, advanced, expert)
    /// @return the help message
    std::string help(const Attribute& max_attribute = Attribute::optional) const;

    /// Get the OptionParser's description
    /// @return the description as given during construction
    std::string description() const;

    /// Get all options that where added with "add"
    /// @return a vector of the contained Options
    const std::vector<Option_ptr>& options() const;

    /// Get command line arguments without option
    /// e.g. "-i 5 hello" => hello
    /// e.g. "-i 5 -- from here non option args" => "from", "here", "non", "option", "args"
    /// @return vector to "stand-alone" command line arguments
    const std::vector<std::string>& non_option_args() const;

    /// Get unknown command options
    /// e.g. '--some_unknown_option="hello"'
    /// @return vector to "stand-alone" command line arguments
    const std::vector<std::string>& unknown_options() const;

    /// Get an Option by it's long name
    /// @param the Option's long name
    /// @return a pointer of type "Value, Switch, Implicit" to the Option or nullptr
    template <typename T>
    std::shared_ptr<T> get_option(const std::string& long_name) const;

    /// Get an Option by it's short name
    /// @param the Option's short name
    /// @return a pointer of type "Value, Switch, Implicit" to the Option or nullptr
    template <typename T>
    std::shared_ptr<T> get_option(char short_name) const;

protected:
    std::vector<Option_ptr> options_;
    std::string description_;
    std::vector<std::string> non_option_args_;
    std::vector<std::string> unknown_options_;

    Option_ptr find_option(const std::string& long_name) const;
    Option_ptr find_option(char short_name) const;
};



class invalid_option : public std::invalid_argument
{
public:
    enum class Error
    {
        missing_argument,
        invalid_argument,
        too_many_arguments,
        missing_option
    };

    invalid_option(const Option* option, invalid_option::Error error, OptionName what_name, std::string value, const std::string& text)
        : std::invalid_argument(text.c_str()), option_(option), error_(error), what_name_(what_name), value_(std::move(value))
    {
    }

    invalid_option(const Option* option, invalid_option::Error error, const std::string& text)
        : invalid_option(option, error, OptionName::unspecified, "", text)
    {
    }

    const Option* option() const
    {
        return option_;
    }

    Error error() const
    {
        return error_;
    }

    OptionName what_name() const
    {
        return what_name_;
    }

    std::string value() const
    {
        return value_;
    }

private:
    const Option* option_;
    Error error_;
    OptionName what_name_;
    std::string value_;
};



/// Base class for an OptionPrinter
/**
 * OptionPrinter creates a help message for a given OptionParser
 */
class OptionPrinter
{
public:
    /// Constructor
    /// @param option_parser the OptionParser to create the help message from
    explicit OptionPrinter(const OptionParser* option_parser) : option_parser_(option_parser)
    {
    }

    /// Destructor
    virtual ~OptionPrinter() = default;

    /// Create a help message
    /// @param max_attribute show options up to this level (optional, advanced, expert)
    /// @return the help message
    virtual std::string print(const Attribute& max_attribute = Attribute::optional) const = 0;

protected:
    const OptionParser* option_parser_;
};



/// Option printer for the console
/**
 * Standard console option printer
 * Creates a human readable help message
 */
class ConsoleOptionPrinter : public OptionPrinter
{
public:
    explicit ConsoleOptionPrinter(const OptionParser* option_parser);
    ~ConsoleOptionPrinter() override = default;

    std::string print(const Attribute& max_attribute = Attribute::optional) const override;

private:
    std::string to_string(const Option_ptr& option) const;
};



/// Option printer for man pages
/**
 * Creates help messages in groff format that can be used in man pages
 */
class GroffOptionPrinter : public OptionPrinter
{
public:
    explicit GroffOptionPrinter(const OptionParser* option_parser);
    ~GroffOptionPrinter() override = default;

    std::string print(const Attribute& max_attribute = Attribute::optional) const override;

private:
    std::string to_string(const Option_ptr& option) const;
};



/// Option printer for bash completion
/**
 * Creates a script with all options (short and long) that can be used for bash completion
 */
class BashCompletionOptionPrinter : public OptionPrinter
{
public:
    BashCompletionOptionPrinter(const OptionParser* option_parser, std::string program_name);
    ~BashCompletionOptionPrinter() override = default;

    std::string print(const Attribute& max_attribute = Attribute::optional) const override;

private:
    std::string program_name_;
};



/// Option implementation /////////////////////////////////

inline Option::Option(const std::string& short_name, const std::string& long_name, std::string description)
    : short_name_(short_name), long_name_(long_name), description_(std::move(description)), attribute_(Attribute::optional)
{
    if (short_name.size() > 1)
        throw std::invalid_argument("length of short name must be <= 1: '" + short_name + "'");

    if (short_name.empty() && long_name.empty())
        throw std::invalid_argument("short and long name are empty");
}


inline char Option::short_name() const
{
    if (!short_name_.empty())
        return short_name_[0];
    return 0;
}


inline std::string Option::long_name() const
{
    return long_name_;
}


inline std::string Option::name(OptionName what_name, bool with_hypen) const
{
    if (what_name == OptionName::short_name)
        return short_name_.empty() ? "" : ((with_hypen ? "-" : "") + short_name_);
    if (what_name == OptionName::long_name)
        return long_name_.empty() ? "" : ((with_hypen ? "--" : "") + long_name_);
    return "";
}


inline std::string Option::description() const
{
    return description_;
}


inline void Option::set_attribute(const Attribute& attribute)
{
    attribute_ = attribute;
}


inline Attribute Option::attribute() const
{
    return attribute_;
}



/// Value implementation /////////////////////////////////

template <class T>
inline Value<T>::Value(const std::string& short_name, const std::string& long_name, const std::string& description)
    : Option(short_name, long_name, description), assign_to_(nullptr)
{
}


template <class T>
inline Value<T>::Value(const std::string& short_name, const std::string& long_name, const std::string& description, const T& default_val, T* assign_to)
    : Value<T>(short_name, long_name, description)
{
    assign_to_ = assign_to;
    set_default(default_val);
}


template <class T>
inline size_t Value<T>::count() const
{
    return values_.size();
}


template <class T>
inline bool Value<T>::is_set() const
{
    return !values_.empty();
}


template <class T>
inline void Value<T>::assign_to(T* var)
{
    assign_to_ = var;
    update_reference();
}


template <class T>
inline void Value<T>::set_value(const T& value)
{
    clear();
    add_value(value);
}

template <class T>
inline T Value<T>::value_or(const T& default_value, size_t idx) const
{
    if (idx < values_.size())
        return values_[idx];
    else if (default_)
        return *default_;
    else
        return default_value;
}

template <class T>
inline T Value<T>::value(size_t idx) const
{
    if (!this->is_set() && default_)
        return *default_;

    if (!is_set() || (idx >= count()))
    {
        std::stringstream optionStr;
        if (!is_set())
            optionStr << "option not set: \"";
        else
            optionStr << "index out of range (" << idx << ") for \"";

        if (short_name() != 0)
            optionStr << "-" << short_name();
        else
            optionStr << "--" << long_name();

        optionStr << "\"";
        throw std::out_of_range(optionStr.str());
    }

    return values_[idx];
}



template <class T>
inline void Value<T>::set_default(const T& value)
{
    this->default_.reset(new T);
    *this->default_ = value;
    update_reference();
}


template <class T>
inline bool Value<T>::has_default() const
{
    return (this->default_ != nullptr);
}


template <class T>
inline T Value<T>::get_default() const
{
    if (!has_default())
        throw std::runtime_error("no default value set");
    return *this->default_;
}


template <class T>
inline bool Value<T>::get_default(std::ostream& out) const
{
    if (!has_default())
        return false;
    out << *this->default_;
    return true;
}


template <class T>
inline Argument Value<T>::argument_type() const
{
    return Argument::required;
}


template <>
inline void Value<std::string>::parse(OptionName what_name, const char* value)
{
    if (strlen(value) == 0)
        throw invalid_option(this, invalid_option::Error::missing_argument, what_name, value, "missing argument for " + name(what_name, true));

    add_value(value);
}


template <>
inline void Value<bool>::parse(OptionName /*what_name*/, const char* value)
{
    bool val =
        ((value != nullptr) && ((strcmp(value, "1") == 0) || (strcmp(value, "true") == 0) || (strcmp(value, "True") == 0) || (strcmp(value, "TRUE") == 0)));
    add_value(val);
}


template <class T>
inline void Value<T>::parse(OptionName what_name, const char* value)
{
    T parsed_value;
    std::string strValue;
    if (value != nullptr)
        strValue = value;

    std::istringstream is(strValue);
    int valuesRead = 0;
    while (is.good())
    {
        if (is.peek() != EOF)
            is >> parsed_value;
        else
            break;

        valuesRead++;
    }

    if (is.fail())
        throw invalid_option(this, invalid_option::Error::invalid_argument, what_name, value,
                             "invalid argument for " + name(what_name, true) + ": '" + strValue + "'");

    if (valuesRead > 1)
        throw invalid_option(this, invalid_option::Error::too_many_arguments, what_name, value,
                             "too many arguments for " + name(what_name, true) + ": '" + strValue + "'");

    if (strValue.empty())
        throw invalid_option(this, invalid_option::Error::missing_argument, what_name, "", "missing argument for " + name(what_name, true));

    this->add_value(parsed_value);
}


template <class T>
inline void Value<T>::update_reference()
{
    if (this->assign_to_)
    {
        if (!this->is_set() && default_)
            *this->assign_to_ = *default_;
        else if (this->is_set())
            *this->assign_to_ = values_.back();
    }
}


template <class T>
inline void Value<T>::add_value(const T& value)
{
    values_.push_back(value);
    update_reference();
}


template <class T>
inline void Value<T>::clear()
{
    values_.clear();
    update_reference();
}



/// Implicit implementation /////////////////////////////////

template <class T>
inline Implicit<T>::Implicit(const std::string& short_name, const std::string& long_name, const std::string& description, const T& implicit_val, T* assign_to)
    : Value<T>(short_name, long_name, description, implicit_val, assign_to)
{
}


template <class T>
inline Argument Implicit<T>::argument_type() const
{
    return Argument::optional;
}


template <class T>
inline void Implicit<T>::parse(OptionName what_name, const char* value)
{
    if ((value != nullptr) && (strlen(value) > 0))
        Value<T>::parse(what_name, value);
    else
        this->add_value(*this->default_);
}



/// Switch implementation /////////////////////////////////

inline Switch::Switch(const std::string& short_name, const std::string& long_name, const std::string& description, bool* assign_to)
    : Value<bool>(short_name, long_name, description, false, assign_to)
{
}


inline void Switch::parse(OptionName /*what_name*/, const char* /*value*/)
{
    add_value(true);
}


inline Argument Switch::argument_type() const
{
    return Argument::no;
}



/// OptionParser implementation /////////////////////////////////

inline OptionParser::OptionParser(std::string description) : description_(std::move(description))
{
}


template <typename T, typename... Ts>
inline std::shared_ptr<T> OptionParser::add(Ts&&... params)
{
    return add<T, Attribute::optional>(std::forward<Ts>(params)...);
}


template <typename T, Attribute attribute, typename... Ts>
inline std::shared_ptr<T> OptionParser::add(Ts&&... params)
{
    static_assert(std::is_base_of<Option, typename std::decay<T>::type>::value, "type T must be Switch, Value or Implicit");
    std::shared_ptr<T> option = std::make_shared<T>(std::forward<Ts>(params)...);

    for (const auto& o : options_)
    {
        if ((option->short_name() != 0) && (option->short_name() == o->short_name()))
            throw std::invalid_argument("duplicate short option name '-" + std::string(1, option->short_name()) + "'");
        if (!option->long_name().empty() && (option->long_name() == (o->long_name())))
            throw std::invalid_argument("duplicate long option name '--" + option->long_name() + "'");
    }
    option->set_attribute(attribute);
    options_.push_back(option);
    return option;
}


inline std::string OptionParser::description() const
{
    return description_;
}


inline const std::vector<Option_ptr>& OptionParser::options() const
{
    return options_;
}


inline const std::vector<std::string>& OptionParser::non_option_args() const
{
    return non_option_args_;
}


inline const std::vector<std::string>& OptionParser::unknown_options() const
{
    return unknown_options_;
}


inline Option_ptr OptionParser::find_option(const std::string& long_name) const
{
    for (const auto& option : options_)
        if (option->long_name() == long_name)
            return option;
    return nullptr;
}


inline Option_ptr OptionParser::find_option(char short_name) const
{
    for (const auto& option : options_)
        if (option->short_name() == short_name)
            return option;
    return nullptr;
}


template <typename T>
inline std::shared_ptr<T> OptionParser::get_option(const std::string& long_name) const
{
    Option_ptr option = find_option(long_name);
    if (!option)
        throw std::invalid_argument("option not found: " + long_name);
    auto result = std::dynamic_pointer_cast<T>(option);
    if (!result)
        throw std::invalid_argument("cannot cast option to T: " + long_name);
    return result;
}


template <typename T>
inline std::shared_ptr<T> OptionParser::get_option(char short_name) const
{
    Option_ptr option = find_option(short_name);
    if (!option)
        throw std::invalid_argument("option not found: " + std::string(1, short_name));
    auto result = std::dynamic_pointer_cast<T>(option);
    if (!result)
        throw std::invalid_argument("cannot cast option to T: " + std::string(1, short_name));
    return result;
}

inline void OptionParser::parse(const std::string& ini_filename)
{
    std::ifstream file(ini_filename.c_str());
    if (file.fail())
        throw std::invalid_argument("cannot open file: '" + ini_filename + "'");

    auto trim = [](std::string& s)
    {
        s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); }));
        s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(), s.end());
        return s;
    };

    auto trim_copy = [trim](const std::string& s)
    {
        std::string copy(s);
        return trim(copy);
    };

    auto split = [trim_copy](const std::string& s) -> std::pair<std::string, std::string>
    {
        size_t pos = s.find('=');
        if (pos == std::string::npos)
            return {"", ""};
        return {trim_copy(s.substr(0, pos)), trim_copy(s.substr(pos + 1, std::string::npos))};
    };

    std::string line;
    std::string section;
    while (std::getline(file, line))
    {
        trim(line);
        if (line.empty())
            continue;
        if (line.front() == '#')
            continue;

        if ((line.front() == '[') && (line.back() == ']'))
        {
            section = trim_copy(line.substr(1, line.size() - 2));
            continue;
        }
        auto key_value = split(line);
        if (key_value.first.empty())
            continue;

        std::string key = section.empty() ? key_value.first : section + "." + key_value.first;
        Option_ptr option = find_option(key);
        if (option && (option->attribute() == Attribute::inactive))
            option = nullptr;

        if (option)
            option->parse(OptionName::long_name, key_value.second.c_str());
        else
            unknown_options_.push_back(key);
    }
}

inline void OptionParser::parse(int argc, const char* const argv[])
{
    for (int n = 1; n < argc; ++n)
    {
        const std::string arg(argv[n]);
        if (arg == "--")
        {
            /// from here on only non opt args
            for (int m = n + 1; m < argc; ++m)
                non_option_args_.emplace_back(argv[m]);
        }
        else if (arg.find("--") == 0)
        {
            /// long option arg
            std::string opt = arg.substr(2);
            std::string optarg;
            size_t equalIdx = opt.find('=');
            if (equalIdx != std::string::npos)
            {
                optarg = opt.substr(equalIdx + 1);
                opt.resize(equalIdx);
            }

            Option_ptr option = find_option(opt);
            if (option && (option->attribute() == Attribute::inactive))
                option = nullptr;
            if (option)
            {
                if (option->argument_type() == Argument::no)
                {
                    if (!optarg.empty())
                        option = nullptr;
                }
                else if (option->argument_type() == Argument::required)
                {
                    if (optarg.empty() && n < argc - 1)
                        optarg = argv[++n];
                }
            }

            if (option)
                option->parse(OptionName::long_name, optarg.c_str());
            else
                unknown_options_.push_back(arg);
        }
        else if (arg.find('-') == 0)
        {
            /// short option arg
            std::string opt = arg.substr(1);
            bool unknown = false;
            for (size_t m = 0; m < opt.size(); ++m)
            {
                char c = opt[m];
                std::string optarg;

                Option_ptr option = find_option(c);
                if (option && (option->attribute() == Attribute::inactive))
                    option = nullptr;
                if (option)
                {
                    if (option->argument_type() == Argument::required)
                    {
                        /// use the rest of the current argument as optarg
                        optarg = opt.substr(m + 1);
                        /// or the next arg
                        if (optarg.empty() && n < argc - 1)
                            optarg = argv[++n];
                        m = opt.size();
                    }
                    else if (option->argument_type() == Argument::optional)
                    {
                        /// use the rest of the current argument as optarg
                        optarg = opt.substr(m + 1);
                        m = opt.size();
                    }
                }

                if (option)
                    option->parse(OptionName::short_name, optarg.c_str());
                else
                    unknown = true;
            }
            if (unknown)
                unknown_options_.push_back(arg);
        }
        else
        {
            non_option_args_.push_back(arg);
        }
    }

    for (auto& opt : options_)
    {
        if ((opt->attribute() == Attribute::required) && !opt->is_set())
        {
            std::string option = opt->long_name().empty() ? std::string(1, opt->short_name()) : opt->long_name();
            throw invalid_option(opt.get(), invalid_option::Error::missing_option, "option \"" + option + "\" is required");
        }
    }
}


inline void OptionParser::reset()
{
    unknown_options_.clear();
    non_option_args_.clear();
    for (auto& opt : options_)
        opt->clear();
}


inline std::string OptionParser::help(const Attribute& max_attribute) const
{
    ConsoleOptionPrinter option_printer(this);
    return option_printer.print(max_attribute);
}



/// ConsoleOptionPrinter implementation /////////////////////////////////

inline ConsoleOptionPrinter::ConsoleOptionPrinter(const OptionParser* option_parser) : OptionPrinter(option_parser)
{
}


inline std::string ConsoleOptionPrinter::to_string(const Option_ptr& option) const
{
    std::stringstream line;
    if (option->short_name() != 0)
    {
        line << "  -" << option->short_name();
        if (!option->long_name().empty())
            line << ", ";
    }
    else
        line << "  ";
    if (!option->long_name().empty())
        line << "--" << option->long_name();

    if (option->argument_type() == Argument::required)
    {
        line << " arg";
        std::stringstream defaultStr;
        if (option->get_default(defaultStr))
        {
            if (!defaultStr.str().empty() && (defaultStr.str() != "\"\""))
                line << " (=" << defaultStr.str() << ")";
        }
    }
    else if (option->argument_type() == Argument::optional)
    {
        std::stringstream defaultStr;
        if (option->get_default(defaultStr))
            line << " [=arg(=" << defaultStr.str() << ")]";
    }

    return line.str();
}


inline std::string ConsoleOptionPrinter::print(const Attribute& max_attribute) const
{
    if (option_parser_ == nullptr)
        return "";

    if (max_attribute < Attribute::optional)
        throw std::invalid_argument("attribute must be 'optional', 'advanced', or 'default'");

    std::stringstream s;
    if (!option_parser_->description().empty())
        s << option_parser_->description() << "\n";

    size_t optionRightMargin(20);
    const size_t maxDescriptionLeftMargin(40);
    //	const size_t descriptionRightMargin(80);

    for (const auto& option : option_parser_->options())
        optionRightMargin = std::max(optionRightMargin, to_string(option).size() + 2);
    optionRightMargin = std::min(maxDescriptionLeftMargin - 2, optionRightMargin);

    for (const auto& option : option_parser_->options())
    {
        if ((option->attribute() <= Attribute::hidden) || (option->attribute() > max_attribute))
            continue;
        std::string optionStr = to_string(option);
        if (optionStr.size() < optionRightMargin)
            optionStr.resize(optionRightMargin, ' ');
        else
            optionStr += "\n" + std::string(optionRightMargin, ' ');
        s << optionStr;

        std::string line;
        std::vector<std::string> lines;
        std::stringstream description(option->description());
        while (std::getline(description, line, '\n'))
            lines.push_back(line);

        std::string empty(optionRightMargin, ' ');
        for (size_t n = 0; n < lines.size(); ++n)
        {
            if (n > 0)
                s << "\n" << empty;
            s << lines[n];
        }
        s << "\n";
    }

    return s.str();
}



/// GroffOptionPrinter implementation /////////////////////////////////

inline GroffOptionPrinter::GroffOptionPrinter(const OptionParser* option_parser) : OptionPrinter(option_parser)
{
}


inline std::string GroffOptionPrinter::to_string(const Option_ptr& option) const
{
    std::stringstream line;
    if (option->short_name() != 0)
    {
        line << "-" << option->short_name();
        if (!option->long_name().empty())
            line << ", ";
    }
    if (!option->long_name().empty())
        line << "--" << option->long_name();

    if (option->argument_type() == Argument::required)
    {
        line << " arg";
        std::stringstream defaultStr;
        if (option->get_default(defaultStr))
        {
            if (!defaultStr.str().empty())
                line << " (=" << defaultStr.str() << ")";
        }
    }
    else if (option->argument_type() == Argument::optional)
    {
        std::stringstream defaultStr;
        if (option->get_default(defaultStr))
            line << " [=arg(=" << defaultStr.str() << ")]";
    }

    return line.str();
}


inline std::string GroffOptionPrinter::print(const Attribute& max_attribute) const
{
    if (option_parser_ == nullptr)
        return "";

    if (max_attribute < Attribute::optional)
        throw std::invalid_argument("attribute must be 'optional', 'advanced', or 'default'");

    std::stringstream s;
    if (!option_parser_->description().empty())
        s << ".SS " << option_parser_->description() << ":\n";

    for (const auto& option : option_parser_->options())
    {
        if ((option->attribute() <= Attribute::hidden) || (option->attribute() > max_attribute))
            continue;
        s << ".TP\n\\fB" << to_string(option) << "\\fR\n";
        if (!option->description().empty())
            s << option->description() << "\n";
    }

    return s.str();
}



/// BashCompletionOptionPrinter implementation /////////////////////////////////

inline BashCompletionOptionPrinter::BashCompletionOptionPrinter(const OptionParser* option_parser, std::string program_name)
    : OptionPrinter(option_parser), program_name_(std::move(program_name))
{
}


inline std::string BashCompletionOptionPrinter::print(const Attribute& /*max_attribute*/) const
{
    if (option_parser_ == nullptr)
        return "";

    std::stringstream s;
    s << "_" << program_name_ << "()\n";
    s << R"({
	local cur prev opts
	COMPREPLY=()
	cur="${COMP_WORDS[COMP_CWORD]}"
	prev="${COMP_WORDS[COMP_CWORD-1]}"
	opts=")";

    for (const auto& option : option_parser_->options())
    {
        if (option->attribute() > Attribute::hidden)
        {
            if (option->short_name() != 0)
                s << "-" << option->short_name() << " ";
            if (!option->long_name().empty())
                s << "--" << option->long_name() << " ";
        }
    }

    s << R"("
	if [[ ${cur} == -* ]] ; then
		COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
		return 0
	fi
}
complete -F )";
    s << "_" << program_name_ << " " << program_name_ << "\n";

    return s.str();
}



static inline std::ostream& operator<<(std::ostream& out, const OptionParser& op)
{
    return out << op.help();
}


} // namespace popl


#endif // POPL_HPP
