// Copyright 2022, FOSS-VG Developers and Contributers
//
// Author(s):
//  BodgeMaster, Shwoomple
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// version 3 along with this program.
// If not, see https://www.gnu.org/licenses/agpl-3.0.en.html

#include <string>
#include <vector>
#include <map>

#include "cli.hpp"
#include "error.hpp"

namespace CLI {
    Flag::Flag() {
        this->present = false;
    }
    Flag::Flag(char shortName, std::string longName, std::string description) {
        this->shortName = shortName;
        this->longName = longName;
        this->description = description;
        this->present = false;
    }

    Option::Option() {
        this->present = false;
    }
    Option::Option(char shortName, std::string longName, std::string placeholder, std::string description) {
        this->shortName = shortName;
        this->longName = longName;
        this->description = description;
        this->placeholder = placeholder;
        this->present = false;
    }

    Argument::Argument() {
        this->present = false;
    }
    Argument::Argument(std::string placeholder, std::string description) {
        this->description = description;
        this->placeholder = placeholder;
        this->present = false;
    }

    // using int here bc that's how main() is defined
    ArgumentsParser::ArgumentsParser(int argc, const char* const argv[], std::vector<Flag> flags, std::vector<Option> options, std::vector<Argument> arguments) {
        this->wrongUsage = false;
        this->wrongUsageMessages = std::vector<std::string>();
        this->programName = std::string(argv[0]);
        this->arguments = arguments;
        // create lookup tables for all flags and options by their names
        this->flagsByShortName = std::map<char, Flag*>();
        this->flagsByLongName  = std::map<std::string, Flag*>();
        for (Flag flag: flags) {
            Flag* flagPointer = new Flag();
            *flagPointer = flag;
            this->flagsByShortName[flag.shortName] = flagPointer;
            this->flagsByLongName[flag.longName]   = flagPointer;
        }
        this->optionsByShortName = std::map<char, Option*>();
        this->optionsByLongName = std::map<std::string, Option*>();
        for (Option option: options) {
            Option* optionPointer = new Option();
            *optionPointer = option;
            this->optionsByShortName[option.shortName] = optionPointer;
            this->optionsByLongName[option.longName]   = optionPointer;
        }

        Option* optionWaitingForValue = nullptr;
        std::vector<CLI::Argument>::size_type argumentCounter = 0;
        for (int i=1; i<argc; i++) {
            std::string argument(argv[i]);
            if (argument[0]=='-') {
                // do we have unfinished business?
                if (optionWaitingForValue!=nullptr) {
                    this->wrongUsage = true;
                    this->wrongUsageMessages.push_back(std::string("Argument expects value but has none: ")+optionWaitingForValue->longName);
                    optionWaitingForValue = nullptr;
                }
                // long name or short name?
                if (argument[1]=='-') {
                    // long name
                    //FIXME: instead of auto, this should be what string
                    // length is defined as
                    // (std::__cxx11::basic_string<char>::size_type ?)
                    // argument with =value specified?
                    auto position = argument.find("=");
                    if (position==std::string::npos) {
                        // no =value
                        //is option or flag?
                        std::string argumentName = argument.substr(2,argument.length()-2);
                        if (flagsByLongName.contains(argumentName)) {
                            // flag
                            flagsByLongName[argumentName]->present = true;
                        } else if (optionsByLongName.contains(argumentName)) {
                            // option
                            optionsByLongName[argumentName]->present = true;
                            optionWaitingForValue = optionsByLongName[argumentName];
                            if (i+1 == argc) {
                                this->wrongUsage = true;
                                this->wrongUsageMessages.push_back(std::string("Argument expects value but has none: ")+argumentName);
                            }
                        } else {
                            this->wrongUsage = true;
                            this->wrongUsageMessages.push_back(std::string("Unknown argument or flag: ")+argument);
                        }
                    } else {
                        // has =value
                        std::string value = argument.substr(position+1, argument.length()-position-1);
                        std::string argumentName = argument.substr(2, position-2);
                        if (optionsByLongName.contains(argumentName)) {
                            optionsByLongName[argumentName]->present = true;
                            optionsByLongName[argumentName]->value = value;
                        } else {
                            this->wrongUsage = true;
                            this->wrongUsageMessages.push_back(std::string("Unknown argument (or it's a flag that doesn't take a value): ")+argument);
                        }
                    }
                } else {
                    // short name
                    //FIXME: instead of int, this should use what string
                    // length is defined as
                    // (std::__cxx11::basic_string<char>::size_type ?)
                    // starting at 1 because 0 is '-'
                    for (int j=1; j<(int) argument.length(); j++) {
                        // is option or flag?
                        if (flagsByShortName.contains(argument[j])) {
                            // flag
                            flagsByShortName[argument[j]]->present = true;
                        } else if (optionsByShortName.contains(argument[j])) {
                            // option
                            optionsByShortName[argument[j]]->present = true;
                            //FIXME: see above
                            if (j+1==(int) argument.length()) {
                                optionWaitingForValue = optionsByShortName[argument[j]];
                                if (i+1 == argc) {
                                    this->wrongUsage = true;
                                    this->wrongUsageMessages.push_back(std::string("Argument expects value but has none: ")+this->optionsByShortName[argument[j]]->longName);
                                }
                            } else {
                                //assume the rest of the argv is a concatenated argument value
                                optionsByShortName[argument[j]]->value = argument.substr(j+1, argument.length()-j-1);
                                break;
                            }
                        } else {
                            this->wrongUsage = true;
                            this->wrongUsageMessages.push_back(std::string("Unknown argument or flag(s): ")+argument.substr(j, argument.length()-j));
                            // err on the side of caution to ensure that
                            // no unwanted options get activated on programs
                            // that deal gracefully with unrecognized command
                            // line parameters
                            break;
                        }
                    }
                }
            } else {
                // argument or value for option?
                if (optionWaitingForValue==nullptr) {
                    // argument
                    if (argumentCounter < this->arguments.size()) {
                        this->arguments.at(argumentCounter).present = true;
                        this->arguments.at(argumentCounter).value = argument;
                    } else {
                        this->wrongUsage = true;
                        this->wrongUsageMessages.push_back(std::string("Too many arguments! Unexpected encounter of: ")+argument);
                    }
                    argumentCounter++;
                } else {
                    // value for option
                    optionWaitingForValue->value = argument;
                    optionWaitingForValue = nullptr;
                }
            }
        }
        for (Argument const& argument: this->arguments) {
            if (!argument.present) {
                this->wrongUsage = true;
                this->wrongUsageMessages.push_back(std::string("Too few arguments! Missing: ")+argument.placeholder);
            }
        }
    }

    ArgumentsParser::ArgumentsParser(int argc, const char* const argv[], std::vector<Flag> flags, std::vector<Option> options, std::vector<Argument> arguments, std::string description): ArgumentsParser::ArgumentsParser(argc, argv, flags, options, arguments) {
        this->description = description;
    }

    ArgumentsParser::ArgumentsParser(int argc, const char* const argv[], std::vector<Flag> flags, std::vector<Option> options, std::vector<Argument> arguments, std::string description, std::string additionalInfo): ArgumentsParser::ArgumentsParser(argc, argv, flags, options, arguments) {
        this->description = description;
        this->additionalInfo = additionalInfo;
    }

    ArgumentsParser::~ArgumentsParser() {
        //TODO: check that this actually runs
        for (auto const& [shortName, flag]: this->flagsByShortName) {
            delete flag;
        }
        for (auto const& [shortName, option]: this->optionsByShortName) {
            delete option;
        }
    }

    ErrorOr<bool> ArgumentsParser::getFlag(char shortName) {
        if (!this->flagsByShortName.contains(shortName)) return ErrorOr<bool>(true, ErrorCodes::UNKNOWN_KEY, false);
        if (this->wrongUsage) {
            if (this->flagsByShortName[shortName]->present) return ErrorOr<bool>(true, ErrorCodes::WRONG_USAGE, true);
            else return ErrorOr<bool>(true, ErrorCodes::NOT_PRESENT, false);
        }
        if (this->flagsByShortName[shortName]->present) return ErrorOr<bool>(true);
        else return ErrorOr<bool>(false, ErrorCodes::NOT_PRESENT, false);
    }

    ErrorOr<bool> ArgumentsParser::getFlag(std::string longName) {
        if (!this->flagsByLongName.contains(longName)) return ErrorOr<bool>(true, ErrorCodes::UNKNOWN_KEY, false);
        if (this->wrongUsage) {
            if (this->flagsByLongName[longName]->present) return ErrorOr<bool>(true, ErrorCodes::WRONG_USAGE, true);
            else return ErrorOr<bool>(true, ErrorCodes::NOT_PRESENT, false);
        }
        if (this->flagsByLongName[longName]->present) return ErrorOr<bool>(true);
        else return ErrorOr<bool> (false, ErrorCodes::NOT_PRESENT, false);
    }

    ErrorOr<std::string> ArgumentsParser::getArgument(std::vector<CLI::Argument>::size_type position){
        if (position >= this->arguments.size()) return ErrorOr<std::string>(true, ErrorCodes::OUT_OF_RANGE, std::string(""));
        if (this->wrongUsage) {
            if (this->arguments.at(position).present) return ErrorOr<std::string>(true, ErrorCodes::WRONG_USAGE, this->arguments.at(position).value);
            else return ErrorOr<std::string>(true, ErrorCodes::NOT_PRESENT, std::string(""));
        }
        return ErrorOr<std::string>(this->arguments.at(position).value);
    }

    ErrorOr<std::string> ArgumentsParser::getOption(char shortName) {
        if (!this->optionsByShortName.contains(shortName)) return ErrorOr<std::string>(true, ErrorCodes::UNKNOWN_KEY, std::string(""));
        if (this->wrongUsage) {
            if (this->optionsByShortName[shortName]->present) return ErrorOr<std::string>(true, ErrorCodes::WRONG_USAGE, this->optionsByShortName[shortName]->value);
            else return ErrorOr<std::string>(true, ErrorCodes::NOT_PRESENT, std::string(""));
        }
        if (this->optionsByShortName[shortName]->present) return ErrorOr<std::string>(this->optionsByShortName[shortName]->value);
        // argument is not present, but this is not an error -> false, NOT_PRESENT, ""
        else return ErrorOr<std::string>(false, ErrorCodes::NOT_PRESENT, std::string(""));
    }

    ErrorOr<std::string> ArgumentsParser::getOption(std::string longName) {
        if (!this->optionsByLongName.contains(longName)) return ErrorOr<std::string>(true, ErrorCodes::UNKNOWN_KEY, std::string(""));
        if (this->wrongUsage) {
            if (this->optionsByLongName[longName]->present) return ErrorOr<std::string>(true, ErrorCodes::WRONG_USAGE, this->optionsByLongName[longName]->value);
            else return ErrorOr<std::string>(true, ErrorCodes::NOT_PRESENT, std::string(""));
        }
        if (this->optionsByLongName[longName]->present) return ErrorOr<std::string>(this->optionsByLongName[longName]->value);
        // argument is not present, but this is not an error -> false, NOT_PRESENT, ""
        else return ErrorOr<std::string>(false, ErrorCodes::NOT_PRESENT, std::string(""));
    }

    std::string ArgumentsParser::getUsage(){
        std::string usageString = "";
        if (this->description != "") {
            usageString += "Help: " + this->programName + "\n\n\t" + this->description + "\n\n";
        }
        usageString += "Usage: " + this->programName + " ";

        if(!this->flagsByShortName.empty()){
            usageString += "[-";
        }

        for(const auto& [key, value]: this->flagsByShortName){
            usageString.push_back(key);
        }

        if(!this->flagsByShortName.empty()){
            usageString += "] ";
        }

        for(const auto& [key, value]: this->optionsByShortName){
            usageString += "[-";
            usageString.push_back(key);
            usageString += " " + value->placeholder + "] ";
        }

        for(const auto& argument: this->arguments){
            usageString += argument.placeholder + " ";
        }

        usageString.push_back('\n');

        if(!this->flagsByShortName.empty()){
            usageString += "\nFlags:\n";

            for(const auto& [key, value]: this->flagsByShortName){
                usageString += "\t-";
                usageString.push_back(key);
                usageString += ", --" + value->longName + "\n\t\t" + value->description + "\n";
            }
        }

        if(!this->optionsByShortName.empty()){
            usageString += "\nOptions:\n";

            for(const auto& [key, value]: this->optionsByShortName){
                usageString += "\t-";
                usageString.push_back(key);
                usageString += " " + value->placeholder + ", --" + value->longName + "=" + value->placeholder + "\n\t\t" + value->description + "\n";
            }
        }

        if(!this->arguments.empty()){
            usageString += "\nArguments:\n";

            for(const auto& argument: this->arguments){
                usageString += "\t" + argument.placeholder + "\n\t\t" + argument.description + "\n";
            }
        }

        if (this->additionalInfo != "") {
            usageString += "\nAdditional Info:\n\n\t" + this->additionalInfo + "\n";
        }

        return usageString;
    }
}