#include <algorithm>
#include <array>
#include <list>
#include <cassert>
#include <string>
#include <set>
#include <limits>

#include "argparse.hpp"
#include "argparse_util.hpp"

namespace argparse {


    /*
     * ArgumentParser
     */

    ArgumentParser::ArgumentParser(std::string prog_name, std::string description_str, std::ostream& os)
        : description_(description_str)
        , formatter_(new DefaultFormatter())
        , os_(os)
        {
        prog(prog_name);
        argument_groups_.push_back(ArgumentGroup("arguments"));
    }

    ArgumentParser& ArgumentParser::prog(std::string prog_name, bool basename_only) {
        if (basename_only) {
            prog_ = basename(prog_name);
        } else {
            prog_ = prog_name;
        }
        return *this;
    }

    ArgumentParser& ArgumentParser::version(std::string version_str) {
        version_ = version_str;
        return *this;
    }

    ArgumentParser& ArgumentParser::epilog(std::string epilog_str) {
        epilog_ = epilog_str;
        return *this;
    }

    ArgumentGroup& ArgumentParser::add_argument_group(std::string description_str) {
        argument_groups_.push_back(ArgumentGroup(description_str));
        return argument_groups_[argument_groups_.size() - 1];
    }

    void ArgumentParser::parse_args(int argc, const char* const* argv, int error_exit_code, int help_exit_code, int version_exit_code) {
        try {
            parse_args_throw(argc, argv);
        } catch (const argparse::ArgParseHelp&) {
            //Help requested
            print_help();
            std::exit(help_exit_code);
        } catch (const argparse::ArgParseVersion&) {
            print_version();
            std::exit(version_exit_code);
        } catch (const argparse::ArgParseError& e) {
            //Failed to parse
            std::cout << e.what() << "\n";

            std::cout << "\n";
            print_usage();
            std::exit(error_exit_code);
        }
    }

    void ArgumentParser::parse_args_throw(int argc, const char* const* argv) {
        std::vector<std::string> arg_strs;
        for (int i = 1; i < argc; ++i) {
            arg_strs.push_back(argv[i]);
        }

        parse_args_throw(arg_strs);
    }
    
    void ArgumentParser::parse_args_throw(std::vector<std::string> arg_strs) {
        add_help_option_if_unspecified();

        //Reset all the defaults
        for (const auto& group : argument_groups()) {
            for (const auto& arg : group.arguments()) {
                if (arg->default_set()) {
                    arg->set_dest_to_default();
                }
            }
        }

        //Create a look-up of expected argument strings and positional arguments
        std::map<std::string,std::shared_ptr<Argument>> str_to_option_arg;
        std::list<std::shared_ptr<Argument>> positional_args;
        for (const auto& group : argument_groups()) {
            for (const auto& arg : group.arguments()) {
                if (arg->positional()) {
                    positional_args.push_back(arg);
                } else {
                    for (const auto& opt : {arg->long_option(), arg->short_option()}) {
                        if (opt.empty()) continue;

                        auto ret = str_to_option_arg.insert(std::make_pair(opt, arg));

                        if (!ret.second) {
                            //Option string already specified
                            std::stringstream ss;
                            ss << "Option string '" << opt << "' maps to multiple options";
                            throw ArgParseError(ss.str());
                        }
                    }
                }
            }
        }

        std::set<std::shared_ptr<Argument>> specified_arguments;

        //Process the arguments
        for (size_t i = 0; i < arg_strs.size(); i++) {
            ShortArgInfo short_arg_info = no_space_short_arg(arg_strs[i], str_to_option_arg);

            std::shared_ptr<Argument> arg;

            if (short_arg_info.is_no_space_short_arg) {
                //Short argument with no space between value
                arg = short_arg_info.arg;
            } else { //Full argument
                auto iter = str_to_option_arg.find(arg_strs[i]);
                if (iter != str_to_option_arg.end()) {
                    arg = iter->second;
                }
            }

            if (arg) {
                //Start of an argument

                specified_arguments.insert(arg);

                if (arg->action() == Action::STORE_TRUE) {
                    arg->set_dest_to_true(); 
                } else if (arg->action() == Action::STORE_FALSE) {
                    arg->set_dest_to_false();
                } else if (arg->action() == Action::HELP) {
                    arg->set_dest_to_true(); 
                    throw ArgParseHelp();
                } else if (arg->action() == Action::VERSION) {
                    arg->set_dest_to_true(); 
                    throw ArgParseVersion();
                } else {
                    assert(arg->action() == Action::STORE);


                    size_t max_values_to_read = 0;
                    size_t min_values_to_read = 0;
                    if (arg->nargs() == '1') {
                        max_values_to_read = 1;
                        min_values_to_read = 1;
                    } else if (arg->nargs() == '?') {
                        max_values_to_read = 1;
                        min_values_to_read = 0;
                    } else if (arg->nargs() == '*') {
                        max_values_to_read = std::numeric_limits<size_t>::max();
                        min_values_to_read = 0;
                    } else {
                        assert (arg->nargs() == '+');
                        max_values_to_read = std::numeric_limits<size_t>::max();
                        min_values_to_read = 1;
                    }

                    std::vector<std::string> values;
                    size_t nargs_read = 0;
                    if (short_arg_info.is_no_space_short_arg) {
                        //It is a short argument, we already have the first value
                        if (!short_arg_info.value.empty()) {
                            values.push_back(short_arg_info.value);
                            ++nargs_read;
                        }
                    }
                    for (; nargs_read < max_values_to_read; ++nargs_read) {
                        size_t next_idx = i + 1 + nargs_read;
                        if (next_idx >= arg_strs.size()) {
                            break;
                        }
                        std::string str = arg_strs[next_idx];


                        if (is_argument(str, str_to_option_arg)) break;

                        if (!arg->is_valid_value(str)) break;

                        values.push_back(str);
                    }

                    if (nargs_read < min_values_to_read) {

                        if (arg->nargs() == '1') {
                            std::stringstream msg;
                            msg << "Missing expected argument for " << arg_strs[i] << "";
                            throw ArgParseError(msg.str());

                        } else {
                            std::stringstream msg;
                            msg << "Expected at least " << min_values_to_read << " value";
                            if (min_values_to_read > 1) {
                                msg << "s";
                            }
                            msg << " for argument '" << arg_strs[i] << "'";
                            msg << " (found " << values.size() << ")";
                            throw ArgParseError(msg.str());
                        }
                    }
                    assert (nargs_read <= max_values_to_read);

                    for (const auto& val : values) {
                        if (!is_valid_choice(val, arg->choices())) {
                            std::stringstream msg;
                            msg << "Unexpected option value '" << values[0] << "' (expected one of: " << join(arg->choices(), ", ");
                            msg << ") for " << arg->name();
                            throw ArgParseError(msg.str());
                        }
                    }

                    //Set the option values appropriately
                    if (arg->nargs() == '1') {
                        assert(nargs_read == 1);
                        assert(values.size() == 1);


                        try {
                            arg->set_dest_to_value(values[0]); 
                        } catch (const ArgParseConversionError& e) {
                            std::stringstream msg;
                            msg << e.what() << " for " << arg->long_option();
                            auto short_opt = arg->short_option();
                            if (!short_opt.empty()) {
                                msg << "/" << short_opt;
                            }
                            throw ArgParseConversionError(msg.str());
                        }
                    } else if (arg->nargs() == '+' || arg->nargs() == '*') {
                        if (arg->nargs() == '+') {
                            assert(nargs_read >= 1);
                            assert(values.size() >= 1);
                        }

                        for (const auto& value : values) {
                            try {
                                arg->add_value_to_dest(value); 
                            } catch (const ArgParseConversionError& e) {
                                std::stringstream msg;
                                msg << e.what() << " for " << arg->long_option();
                                auto short_opt = arg->short_option();
                                if (!short_opt.empty()) {
                                    msg << "/" << short_opt;
                                }
                                throw ArgParseConversionError(msg.str());
                            }
                        }
                    } else {
                        std::stringstream msg;
                        msg << "Unsupport nargs value '" << arg->nargs() << "'";
                        throw ArgParseError(msg.str());
                    }

                    if (!short_arg_info.is_no_space_short_arg) {
                        i += nargs_read; //Skip over the values (don't need to for short args)
                    }
                }

            } else {
                if (positional_args.empty()) {
                    //Unrecognized
                    std::stringstream ss;
                    ss << "Unexpected command-line argument '" << arg_strs[i] << "'";
                    throw ArgParseError(ss.str());
                } else {
                    //Positional argument
                    auto pos_arg = positional_args.front();
                    positional_args.pop_front();

                    try {
                        pos_arg->set_dest_to_value(arg_strs[i]); 
                    } catch (const ArgParseConversionError& e) {
                        std::stringstream msg;
                        msg << e.what() << " for positional argument " << pos_arg->long_option();
                        throw ArgParseConversionError(msg.str());
                    }

                    auto value = arg_strs[i];
                    specified_arguments.insert(pos_arg);
                }
            }
        }

        //Missing positionals?
        for(const auto& remaining_positional : positional_args) {
            std::stringstream ss;
            ss << "Missing required positional argument: " << remaining_positional->long_option();
            throw ArgParseError(ss.str());
        }

        //Missing required?
        for (const auto& group : argument_groups()) {
            for (const auto& arg : group.arguments()) {
                if (arg->required()) {
                    //potentially slow...
                    if (!specified_arguments.count(arg)) {
                        std::stringstream msg;
                        msg << "Missing required argument: " << arg->long_option();
                        auto short_opt = arg->short_option();
                        if (!short_opt.empty()) {
                            msg << "/" << short_opt;
                        }
                        throw ArgParseError(msg.str());
                    }
                }
            }
        }
    }

    void ArgumentParser::reset_destinations() {
        for (const auto& group : argument_groups()) {
            for (const auto& arg : group.arguments()) {
                arg->reset_dest();
            }
        }
    }

    void ArgumentParser::print_usage() {
        formatter_->set_parser(this);
        os_ << formatter_->format_usage();
    }

    void ArgumentParser::print_help() {
        formatter_->set_parser(this);
        os_ << formatter_->format_usage();
        os_ << formatter_->format_description();
        os_ << formatter_->format_arguments();
        os_ << formatter_->format_epilog();
    }

    void ArgumentParser::print_version() {
        formatter_->set_parser(this);
        os_ << formatter_->format_version();
    }

    std::string ArgumentParser::prog() const { return prog_; }
    std::string ArgumentParser::version() const { return version_; }
    std::string ArgumentParser::description() const { return description_; }
    std::string ArgumentParser::epilog() const { return epilog_; }
    std::vector<ArgumentGroup> ArgumentParser::argument_groups() const { return argument_groups_; }

    void ArgumentParser::add_help_option_if_unspecified() {
        //Has a help already been specified
        bool found_help = false;
        for(auto& grp : argument_groups_) {
            for(auto& arg : grp.arguments()) {
                if(arg->action() == Action::HELP) {
                    found_help = true;
                    break;
                }
            }
        }

        if (!found_help) {

            auto& grp = argument_groups_[0];

            grp.add_argument(show_help_dummy_, "--help", "-h")
                .help("Shows this help message")
                .action(Action::HELP);
        }
    }

    ArgumentParser::ShortArgInfo ArgumentParser::no_space_short_arg(std::string str, const std::map<std::string, std::shared_ptr<Argument>>& str_to_option_arg) const {

        ShortArgInfo short_arg_info;
        for(const auto& kv : str_to_option_arg) {
            if (kv.first.size() == 2) {
                //Is a short arg
                
                //String starts with short arg
                bool match = true;
                for (size_t i = 0; i < kv.first.size(); ++i) {
                    if (kv.first[i] != str[i]) {
                        match = false;
                        break;
                    }
                }

                bool no_space_between_short_arg_and_value = str.size() > kv.first.size();

                if (match && no_space_between_short_arg_and_value) {
                    //Only handles cases where there is no space between short arg and value
                    short_arg_info.is_no_space_short_arg = true;
                    short_arg_info.arg = kv.second;
                    short_arg_info.value = std::string(str.begin() + kv.first.size(), str.end());

                    return short_arg_info;
                }
            }
        }

        assert(!short_arg_info.is_no_space_short_arg);
        return short_arg_info;
    }

    /*
     * ArgumentGroup
     */
    ArgumentGroup::ArgumentGroup(std::string name_str)
        : name_(name_str)
        {}

    ArgumentGroup& ArgumentGroup::epilog(std::string str) {
        epilog_ = str;
        return *this;
    }
    std::string ArgumentGroup::name() const { return name_; }
    std::string ArgumentGroup::epilog() const { return epilog_; }
    const std::vector<std::shared_ptr<Argument>>& ArgumentGroup::arguments() const { return arguments_; }

    /*
     * Argument
     */
    Argument::Argument(std::string long_opt, std::string short_opt)
        : long_opt_(long_opt)
        , short_opt_(short_opt) {

        if (long_opt_.size() < 1) {
            throw ArgParseError("Argument must be at least one character long");
        }

        auto dashes_name = split_leading_dashes(long_opt_);

        if (dashes_name[0].size() == 1 && !short_opt_.empty()) {
            throw ArgParseError("Long option must be specified before short option");
        } else if (dashes_name[0].size() > 2) {
            throw ArgParseError("More than two dashes in argument name");
        }

        //Set defaults
        metavar_ = toupper(dashes_name[1]);
    }

    Argument& Argument::help(std::string help_str) {
        help_ = help_str;
        return *this;
    }

    Argument& Argument::nargs(char nargs_type) {
        //TODO: nargs > 1 support: '?', '*', '+'
        auto valid_nargs = {'0', '1', '+', '*'};

        auto iter = std::find(valid_nargs.begin(), valid_nargs.end(), nargs_type);
        if (iter == valid_nargs.end()) {
            throw ArgParseError("Invalid argument to nargs (must be one of: " + join(valid_nargs, ", ") + ")");
        }

        //Ensure nargs is consistent with the action
        if (action() == Action::STORE_FALSE && nargs_type != '0') {
            throw ArgParseError("STORE_FALSE action requires nargs to be '0'");
        } else if (action() == Action::STORE_TRUE && nargs_type != '0') {
            throw ArgParseError("STORE_TRUE action requires nargs to be '0'");
        } else if (action() == Action::HELP && nargs_type != '0') {
            throw ArgParseError("HELP action requires nargs to be '0'");
        } else if (action() == Action::STORE && (nargs_type != '1' && nargs_type != '+' && nargs_type != '*')) {
            throw ArgParseError("STORE action requires nargs to be '1', '+' or '*'");
        }

        nargs_ = nargs_type;

        valid_action();
        return *this;
    }

    Argument& Argument::metavar(std::string metavar_str) {
        metavar_ = metavar_str;
        return *this;
    }

    Argument& Argument::choices(std::vector<std::string> choice_values) {
        choices_ = choice_values;
        return *this;
    }

    Argument& Argument::action(Action action_type) {
        action_ = action_type;

        if (   action_ == Action::STORE_FALSE 
            || action_ == Action::STORE_TRUE 
            || action_ == Action::HELP 
            || action_ == Action::VERSION) {
            this->nargs('0');
        } else if (action_ == Action::STORE) {
            this->nargs('1');
        } else {
            throw ArgParseError("Unrecognized argparse action");
        }

        return *this;
    }

    Argument& Argument::required(bool is_required) {
        required_ = is_required;
        return *this;
    }

    Argument& Argument::default_value(const std::string& value) {
        if (nargs() != '0' && nargs() != '1' && nargs() != '?') {
            std::stringstream msg;
            msg << "Scalar default value not allowed for nargs='" << nargs() << "'";
            throw ArgParseError(msg.str());
        }
        default_value_.clear();
        default_value_.push_back(value);
        default_set_ = true;
        return *this;
    }

    Argument& Argument::default_value(const std::vector<std::string>& values) {
        if (nargs() != '+' && nargs() != '*') {
            std::stringstream msg;
            msg << "Multiple default value not allowed for nargs='" << nargs() << "'";
            throw ArgParseError(msg.str());
        }
        default_value_ = values;
        default_set_ = true;
        return *this;
    }

    Argument& Argument::default_value(const std::initializer_list<std::string>& values) {
        //Convert to vector and process as usual
        return default_value(std::vector<std::string>(values.begin(), values.end()));
    }

    Argument& Argument::group_name(std::string grp) {
        group_name_ = grp;
        return *this;
    }

    Argument& Argument::show_in(ShowIn show) {
        show_in_ = show;
        return *this;
    }

    std::string Argument::name() const { 
        std::string name_str = long_option();
        if (!short_option().empty()) {
            name_str += "/" + short_option();
        }
        return name_str;
    }

    std::string Argument::long_option() const { return long_opt_; }
    std::string Argument::short_option() const { return short_opt_; }
    std::string Argument::help() const { return help_; }
    char Argument::nargs() const { return nargs_; }
    std::string Argument::metavar() const { return metavar_; }
    std::vector<std::string> Argument::choices() const { return choices_; }
    Action Argument::action() const { return action_; }
    std::string Argument::default_value() const { 
        if (default_value_.size() > 1) {
            std::stringstream msg;
            msg << "{" << join(default_value_, ", ") << "}";
            return msg.str();
        } else if (default_value_.size() == 1) {
            return default_value_[0]; 
        } else {
            return "";
        }
    }
    std::string Argument::group_name() const { return group_name_; }
    ShowIn Argument::show_in() const { return show_in_; }
    bool Argument::default_set() const { return default_set_; }

    bool Argument::required() const {
        if(positional()) {
            //Positional arguments are always required
            return true;
        }
        return required_;
    }
    bool Argument::positional() const {
        assert(long_option().size() > 1);
        return long_option()[0] != '-';
    }
} //namespace
