//
// Created by Yukio Fukuzawa on 11/12/18.
//

#ifndef UTILITIES_ARGH_H
#define UTILITIES_ARGH_H

#pragma once

#include <algorithm>
#include <sstream>
#include <string>
#include <vector>
#include <set>
#include <map>
#include <cassert>

namespace argh {
    // Terminology:
    // A command line is composed of 2 types of args:
    // 1. Positional args, i.e. free standing values
    // 2. Options: args beginning with '-'. We identify two kinds:
    //    2.1: Flags: boolean options =>  (exist ? true : false)
    //    2.2: Parameters: a name followed by a non-option value

#if !defined(__GNUC__) || (__GNUC__ >= 5)
    using string_stream = std::istringstream;
#else

    // Until GCC 5, istringstream did not have a move constructor.
    // stringstream_proxy is used instead, as a workaround.
    class stringstream_proxy {
    public:
        stringstream_proxy() = default;

        // Construct with a value.
        stringstream_proxy(std::string const &value) :
                stream_(value) {}

        // Copy constructor.
        stringstream_proxy(const stringstream_proxy &other) :
                stream_(other.stream_.str()) {
            stream_.setstate(other.stream_.rdstate());
        }

        void setstate(std::ios_base::iostate state) { stream_.setstate(state); }

        // Stream out the value of the parameter.
        // If the conversion was not possible, the stream will enter the fail state,
        // and operator bool will return false.
        template<typename T>
        stringstream_proxy &operator>>(T &thing) {
            stream_ >> thing;
            return *this;
        }


        // Get the string value.
        std::string str() const { return stream_.str(); }

        std::stringbuf *rdbuf() const { return stream_.rdbuf(); }

        // Check the state of the stream.
        // False when the most recent stream operation failed
        operator bool() const { return !!stream_; }

        ~stringstream_proxy() = default;

    private:
        std::istringstream stream_;
    };

    using string_stream = stringstream_proxy;
#endif

    class parser {
    public:
        enum Mode {
            PREFER_FLAG_FOR_UNREG_OPTION = 1 << 0,
            PREFER_PARAM_FOR_UNREG_OPTION = 1 << 1,
            NO_SPLIT_ON_EQUALSIGN = 1 << 2,
            SINGLE_DASH_IS_MULTIFLAG = 1 << 3,
        };

        parser() = default;

        parser(std::initializer_list<char const *const> pre_reg_names) { add_params(pre_reg_names); }

        parser(char **argv, int mode = PREFER_FLAG_FOR_UNREG_OPTION) { parse(argv, mode); }

        parser(int argc, char **argv, int mode = PREFER_FLAG_FOR_UNREG_OPTION) { parse(argc, argv, mode); }

        void add_param(std::string const &name);

        void add_params(std::initializer_list<char const *const> init_list);

        void parse(char **argv, int mode = PREFER_FLAG_FOR_UNREG_OPTION);

        void parse(int argc, char **argv, int mode = PREFER_FLAG_FOR_UNREG_OPTION);

        std::multiset<std::string> const &flags() const { return flags_; }

        std::map<std::string, std::string> const &params() const { return params_; }

        std::vector<std::string> const &pos_args() const { return pos_args_; }

        // begin() and end() for using range-for over positional args.
        std::vector<std::string>::const_iterator begin() const { return pos_args_.cbegin(); }

        std::vector<std::string>::const_iterator end() const { return pos_args_.cend(); }

        size_t size() const { return pos_args_.size(); }

        //////////////////////////////////////////////////////////////////////////
        // Accessors

        // flag (boolean) accessors: return true if the flag appeared, otherwise false.
        bool operator[](std::string const &name) const;

        // multiple flag (boolean) accessors: return true if at least one of the flag appeared, otherwise false.
        bool operator[](std::initializer_list<char const *const> init_list) const;

        // returns positional arg string by order. Like argv[] but without the options
        std::string const &operator[](size_t ind) const;

        // returns a std::istream that can be used to convert a positional arg to a typed value.
        string_stream operator()(size_t ind) const;

        // same as above, but with a default value in case the arg is missing (index out of range).
        template<typename T>
        string_stream operator()(size_t ind, T &&def_val) const;

        // parameter accessors, give a name get an std::istream that can be used to convert to a typed value.
        // call .str() on result to get as string
        string_stream operator()(std::string const &name) const;

        // accessor for a parameter with multiple names, give a list of names, get an std::istream that can be used to convert to a typed value.
        // call .str() on result to get as string
        // returns the first value in the list to be found.
        string_stream operator()(std::initializer_list<char const *const> init_list) const;

        // same as above, but with a default value in case the param was missing.
        // Non-string def_val types must have an operator<<() (output stream operator)
        // If T only has an input stream operator, pass the string version of the type as in "3" instead of 3.
        template<typename T>
        string_stream operator()(std::string const &name, T &&def_val) const;

        // same as above but for a list of names. returns the first value to be found.
        template<typename T>
        string_stream operator()(std::initializer_list<char const *const> init_list, T &&def_val) const;

    private:
        string_stream bad_stream() const;

        std::string trim_leading_dashes(std::string const &name) const;

        bool is_number(std::string const &arg) const;

        bool is_option(std::string const &arg) const;

        bool got_flag(std::string const &name) const;

        bool is_param(std::string const &name) const;

    private:
        std::vector<std::string> args_;
        std::map<std::string, std::string> params_;
        std::vector<std::string> pos_args_;
        std::multiset<std::string> flags_;
        std::set<std::string> registeredParams_;
        std::string empty_;
    };


    //////////////////////////////////////////////////////////////////////////

    inline void parser::parse(char **argv, int mode) {
        int argc = 0;
        for (auto argvp = argv; *argvp; ++argc, ++argvp);
        parse(argc, argv, mode);
    }

    //////////////////////////////////////////////////////////////////////////

    inline void parser::parse(int argc, char **argv, int mode /*= PREFER_FLAG_FOR_UNREG_OPTION*/) {
        // convert to strings
        args_.resize(argc);
        std::transform(argv, argv + argc, args_.begin(), [](const char *const arg) { return arg; });

        // parse line
        for (auto i = 0u; i < args_.size(); ++i) {
            if (!is_option(args_[i])) {
                pos_args_.emplace_back(args_[i]);
                continue;
            }

            auto name = trim_leading_dashes(args_[i]);

            if (!(mode & NO_SPLIT_ON_EQUALSIGN)) {
                auto equalPos = name.find('=');
                if (equalPos != std::string::npos) {
                    params_.insert({name.substr(0, equalPos), name.substr(equalPos + 1)});
                    continue;
                }
            }

            // if the option is unregistered and should be a multi-flag
            if (1 == (args_[i].size() - name.size()) &&         // single dash
                argh::parser::SINGLE_DASH_IS_MULTIFLAG & mode && // multi-flag mode
                !is_param(name))                                  // unregistered
            {
                std::string keep_param;

                if (!name.empty() && is_param(std::string(1ul, name.back()))) // last char is param
                {
                    keep_param += name.back();
                    name.resize(name.size() - 1);
                }

                for (auto const &c : name) {
                    flags_.emplace(std::string{c});
                }

                if (!keep_param.empty()) {
                    name = keep_param;
                } else {
                    continue; // do not consider other options for this arg
                }
            }

            // any potential option will get as its value the next arg, unless that arg is an option too
            // in that case it will be determined a flag.
            if (i == args_.size() - 1 || is_option(args_[i + 1])) {
                flags_.emplace(name);
                continue;
            }

            // if 'name' is a pre-registered option, then the next arg cannot be a free parameter to it is skipped
            // otherwise we have 2 modes:
            // PREFER_FLAG_FOR_UNREG_OPTION: a non-registered 'name' is determined a flag.
            //                               The following value (the next arg) will be a free parameter.
            //
            // PREFER_PARAM_FOR_UNREG_OPTION: a non-registered 'name' is determined a parameter, the next arg
            //                                will be the value of that option.

            assert(!(mode & argh::parser::PREFER_FLAG_FOR_UNREG_OPTION)
                   || !(mode & argh::parser::PREFER_PARAM_FOR_UNREG_OPTION));

            bool preferParam = mode & argh::parser::PREFER_PARAM_FOR_UNREG_OPTION;

            if (is_param(name) || preferParam) {
                params_.insert({name, args_[i + 1]});
                ++i; // skip next value, it is not a free parameter
                continue;
            } else {
                flags_.emplace(name);
            }
        };
    }

    //////////////////////////////////////////////////////////////////////////

    inline string_stream parser::bad_stream() const {
        string_stream bad;
        bad.setstate(std::ios_base::failbit);
        return bad;
    }

    //////////////////////////////////////////////////////////////////////////

    inline bool parser::is_number(std::string const &arg) const {
        // inefficient but simple way to determine if a string is a number (which can start with a '-')
        std::istringstream istr(arg);
        double number;
        istr >> number;
        return !(istr.fail() || istr.bad());
    }

    //////////////////////////////////////////////////////////////////////////

    inline bool parser::is_option(std::string const &arg) const {
        assert(0 != arg.size());
        if (is_number(arg))
            return false;
        return '-' == arg[0];
    }

    //////////////////////////////////////////////////////////////////////////

    inline std::string parser::trim_leading_dashes(std::string const &name) const {
        auto pos = name.find_first_not_of('-');
        return std::string::npos != pos ? name.substr(pos) : name;
    }

    //////////////////////////////////////////////////////////////////////////

    inline bool argh::parser::got_flag(std::string const &name) const {
        return flags_.end() != flags_.find(trim_leading_dashes(name));
    }

    //////////////////////////////////////////////////////////////////////////

    inline bool argh::parser::is_param(std::string const &name) const {
        return registeredParams_.count(name);
    }

    //////////////////////////////////////////////////////////////////////////

    inline bool parser::operator[](std::string const &name) const {
        return got_flag(name);
    }

    //////////////////////////////////////////////////////////////////////////

    inline bool parser::operator[](std::initializer_list<char const *const> init_list) const {
        return std::any_of(init_list.begin(), init_list.end(), [&](char const *const name) { return got_flag(name); });
    }

    //////////////////////////////////////////////////////////////////////////

    inline std::string const &parser::operator[](size_t ind) const {
        if (ind < pos_args_.size())
            return pos_args_[ind];
        return empty_;
    }

    //////////////////////////////////////////////////////////////////////////

    inline string_stream parser::operator()(std::string const &name) const {
        auto optIt = params_.find(trim_leading_dashes(name));
        if (params_.end() != optIt)
            return string_stream(optIt->second);
        return bad_stream();
    }

    //////////////////////////////////////////////////////////////////////////

    inline string_stream parser::operator()(std::initializer_list<char const *const> init_list) const {
        for (auto &name : init_list) {
            auto optIt = params_.find(trim_leading_dashes(name));
            if (params_.end() != optIt)
                return string_stream(optIt->second);
        }
        return bad_stream();
    }

    //////////////////////////////////////////////////////////////////////////

    template<typename T>
    string_stream parser::operator()(std::string const &name, T &&def_val) const {
        auto optIt = params_.find(trim_leading_dashes(name));
        if (params_.end() != optIt)
            return string_stream(optIt->second);

        std::ostringstream ostr;
        ostr << def_val;
        return string_stream(ostr.str()); // use default
    }

    //////////////////////////////////////////////////////////////////////////

    // same as above but for a list of names. returns the first value to be found.
    template<typename T>
    string_stream parser::operator()(std::initializer_list<char const *const> init_list, T &&def_val) const {
        for (auto &name : init_list) {
            auto optIt = params_.find(trim_leading_dashes(name));
            if (params_.end() != optIt)
                return string_stream(optIt->second);
        }
        std::ostringstream ostr;
        ostr << def_val;
        return string_stream(ostr.str()); // use default
    }

    //////////////////////////////////////////////////////////////////////////

    inline string_stream parser::operator()(size_t ind) const {
        if (pos_args_.size() <= ind)
            return bad_stream();

        return string_stream(pos_args_[ind]);
    }

    //////////////////////////////////////////////////////////////////////////

    template<typename T>
    string_stream parser::operator()(size_t ind, T &&def_val) const {
        if (pos_args_.size() <= ind) {
            std::ostringstream ostr;
            ostr << def_val;
            return string_stream(ostr.str());
        }

        return string_stream(pos_args_[ind]);
    }

    //////////////////////////////////////////////////////////////////////////

    inline void parser::add_param(std::string const &name) {
        registeredParams_.insert(trim_leading_dashes(name));
    }

    //////////////////////////////////////////////////////////////////////////

    inline void parser::add_params(std::initializer_list<char const *const> init_list) {
        for (auto &name : init_list)
            registeredParams_.insert(trim_leading_dashes(name));
    }
}

#endif //UTILITIES_ARGH_H
