#include <pbdata/CommandLineParser.hpp>

CommandLineParser::CommandLineParser()
{
    lineLength = 80;
    numUnnamedOptions = 0;
    specialVersionFlag = true;
}

void CommandLineParser::SetProgramSummary(std::string summaryp) { programSummary = summaryp; }

void CommandLineParser::SetHelp(std::string _help) { helpString = _help; }

void CommandLineParser::SetConciseHelp(std::string _conciseHelp) { conciseHelp = _conciseHelp; }

void CommandLineParser::SetProgramName(std::string namep) { programName = namep; }

void CommandLineParser::SetVersion(std::string versionp)
{
    specialVersionFlag = true;
    version = versionp;
}

void CommandLineParser::SetVerboseHelp(std::string helpp) { verboseHelp = helpp; }

void CommandLineParser::SetExamples(std::string examplesp) { examples = examplesp; }

void CommandLineParser::RegisterPreviousFlagsAsHidden()
{
    for (size_t i = 0; i < named.size(); i++) {
        named[i] = false;
    }
    numUnnamedOptions = named.size();
}

void CommandLineParser::RegisterVersionFlag(bool *value)
{
    specialVersionFlag = true;
    RegisterFlagOption("version", value, "Print version number.");
}

void CommandLineParser::RegisterFlagOption(std::string option, bool *value, std::string description,
                                           bool required)
{

    named.push_back(true);
    optionList.push_back(option);
    optionTypeList.push_back(Flag);
    optionValueIndexList.push_back(boolValues.size());
    boolValues.push_back(value);
    descriptions.push_back(description);
    optionRequired.push_back(required);
    optionUsed.push_back(false);
}

void CommandLineParser::RegisterIntOption(std::string option, int *value, std::string description,
                                          OptionType type, bool required)
{

    named.push_back(true);
    optionList.push_back(option);
    optionTypeList.push_back(type);
    optionValueIndexList.push_back(intValues.size());
    intValues.push_back(value);
    descriptions.push_back(description);
    optionRequired.push_back(required);
    optionUsed.push_back(false);
}

void CommandLineParser::RegisterFloatOption(std::string option, float *value,
                                            std::string description, OptionType type, bool required)
{

    named.push_back(true);
    optionList.push_back(option);
    optionTypeList.push_back(type);
    optionValueIndexList.push_back(floatValues.size());
    floatValues.push_back(value);
    descriptions.push_back(description);
    optionRequired.push_back(required);
    optionUsed.push_back(false);
}

void CommandLineParser::RegisterStringOption(std::string option, std::string *value,
                                             std::string description, bool required)
{

    named.push_back(true);
    optionList.push_back(option);
    optionTypeList.push_back(String);
    optionValueIndexList.push_back(stringValues.size());
    stringValues.push_back(value);
    descriptions.push_back(description);
    optionRequired.push_back(required);
    optionUsed.push_back(false);
}

void CommandLineParser::RegisterStringListOption(std::string option,
                                                 std::vector<std::string> *value,
                                                 std::string description, bool required)
{

    named.push_back(true);
    optionList.push_back(option);
    optionTypeList.push_back(StringList);
    optionValueIndexList.push_back(stringListValues.size());
    stringListValues.push_back(value);
    descriptions.push_back(description);
    optionRequired.push_back(required);
    optionUsed.push_back(false);
}

void CommandLineParser::RegisterIntListOption(std::string option, std::vector<int> *value,
                                              std::string description, bool required)
{

    named.push_back(true);
    optionList.push_back(option);
    optionTypeList.push_back(IntegerList);
    optionValueIndexList.push_back(intListValues.size());
    intListValues.push_back(value);
    descriptions.push_back(description);
    optionRequired.push_back(required);
    optionUsed.push_back(false);
}

int CommandLineParser::IsOption(char *str)
{
    int len = strlen(str);
    if (len == 0) {
        return 0;
    } else {
        return str[0] == '-';
    }
}

int CommandLineParser::IsInteger(char *str)
{
    int len = strlen(str);
    int i;
    if (len == 0) return 0;
    if (!(str[0] == '-' or (str[0] >= '0' and str[0] <= '9'))) return 0;
    for (i = 1; i < len; i++) {
        if (!isdigit(str[i])) return 0;
    }
    return 1;
}

int CommandLineParser::IsFloat(char *str)
{
    int len = strlen(str);
    int i;
    if (len == 0) {
        return 0;
    }
    int nDot = 0;
    int nDigit = 0;
    for (i = 0; i < len; i++) {
        if (isdigit(str[i])) nDigit++;
        if (str[i] == '.') nDot++;
    }
    if (nDot > 1) return 0;
    if (nDigit == 0) return 0;
    if (!isdigit(str[0]) and str[0] != '-' and str[0] != '.') return 0;
    //
    // passed all checks, ok!
    //
    return 1;
}

int CommandLineParser::FindOption(char *option)
{
    for (size_t i = 0; i < optionList.size(); i++) {
        if (optionList[i].compare(option) == 0) {
            return i;
        }
    }
    return -1;
}

void CommandLineParser::CommandLineToString(int argc, char *argv[], std::string &commandLine)
{

    std::stringstream outstrm;
    int i;
    for (i = 0; i < argc; i++) {
        outstrm << argv[i] << " ";
    }
    commandLine = outstrm.str();
}

int CommandLineParser::ParseCommandLine(int argc, char *argv[], bool isProgramNameOnlyAllowed)
{
    std::vector<std::string> ufv;
    return ParseCommandLine(argc, argv, ufv, isProgramNameOnlyAllowed);
}

int CommandLineParser::ParseCommandLine(int argc, char *argv[],
                                        std::vector<std::string> &unflaggedValues,
                                        bool isProgramNameOnlyAllowed)
{

    int argi = 1;
    int curUnnamedOption = 0;
    ErrorValue ev;
    //
    // Check for a help flag.
    //
    int i;
    for (i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-h") == 0 or (strcmp(argv[i], "--help") == 0 and
                                           // Check to see if there is non default argument for help
                                           IsOption(argv[i]) and !FindOption(&argv[i][1]))) {
            PrintUsage();
            exit(EXIT_SUCCESS);
        } else if (strcmp(argv[i], "--version") == 0 and specialVersionFlag) {
            //
            // Using -version is an early exit since programs will print the
            // version and then return.
            //
            assert(IsOption(argv[i]) and FindOption(&argv[argi][1]));
            PrintVersion();
            exit(EXIT_SUCCESS);
        }
    }

    if (!isProgramNameOnlyAllowed) {
        if (argc == 1 || argc < numUnnamedOptions) {
            if (conciseHelp != "") {
                std::cout << conciseHelp;
            } else {
                PrintUsage();
            }
            exit(EXIT_SUCCESS);
        }
    }

    //
    // Now parse the (probably optional) options.
    //
    while (argi < argc) {
        if (IsOption(argv[argi])) {
            //
            // Find which option is specified.
            //
            int optionIndex = FindOption(&argv[argi][1]);
            if (optionIndex == -1) {
                ev = CLBadOption;
            } else {
                argi++;

                //
                // Record that this option has been specified.
                //
                optionUsed[optionIndex] = true;
                ev = ParseOption(optionIndex, argi, argc, argv);
            }
            if (ev != CLGood) {
                PrintUsage();
                PrintErrorMessage(ev, &argv[argi][0]);
                std::exit(EXIT_FAILURE);
            }
        } else {
            unflaggedValues.push_back(argv[argi]);
            if (curUnnamedOption < numUnnamedOptions) {
                ev = ParseOption(curUnnamedOption, argi, argc, argv);
                optionUsed[curUnnamedOption] = true;
                curUnnamedOption++;
            } else {
                ++argi;
            }
        }
    }

    ev = PrintErrorOnMissingOptions();
    if (ev != CLGood) {
        PrintUsage();
        PrintErrorMessage(ev, &argv[argi][0]);
        std::exit(EXIT_FAILURE);
    }
    return 1;
}

CommandLineParser::ErrorValue CommandLineParser::ParseOption(int optionIndex, int &argi, int argc,
                                                             char *argv[])
{

    ErrorValue ev = CLMissingValue;
    //
    // Extract the value type of this option.
    //
    int optionValueIndex = optionValueIndexList[optionIndex];
    OptionType optionType = optionTypeList[optionIndex];

    switch (optionType) {
        case (Flag):
            ev = ParseFlag(optionValueIndex);
            break;
        case (Integer):
            ev = ParseInteger(optionValueIndex, argi, argc, argv);
            break;
        case (PositiveInteger):
            ev = ParsePositiveInteger(optionValueIndex, argi, argc, argv);
            break;
        case (NonNegativeInteger):
            ev = ParseNonNegativeInteger(optionValueIndex, argi, argc, argv);
            break;
        case (Float):
            ev = ParseFloat(optionValueIndex, argi, argc, argv);
            break;
        case (PositiveFloat):
            ev = ParsePositiveFloat(optionValueIndex, argi, argc, argv);
            break;
        case (NonNegativeFloat):
            ev = ParseNonNegativeFloat(optionValueIndex, argi, argc, argv);
            break;
        case (String):
            ev = ParseString(optionValueIndex, argi, argc, argv);
            break;
        case (StringList):
            ev = ParseStringList(optionValueIndex, argi, argc, argv);
            break;
        case (IntegerList):
            ev = ParseIntList(optionValueIndex, argi, argc, argv);
            break;
    };
    if (ev == CLGood) {
        optionUsed[optionValueIndex] = true;
    }
    return ev;
}

void CommandLineParser::PrintErrorMessage(ErrorValue ev, char *option)
{
    switch (ev) {
        case (CLBadOption):
            std::cout << "ERROR: " << option << " is not a valid option." << std::endl;
            break;
        case (CLMissingValue):
            std::cout << "ERROR: " << option << " requires a value." << std::endl;
            break;
        case (CLInvalidInteger):
            std::cout << "ERROR: " << option << " requires an "
                      << "integer value (...,-2,-1,0,1,2,...)" << std::endl;
            break;
        case (CLInvalidPositiveInteger):
            std::cout << "ERROR: " << option << " requires an integer greater than 0." << std::endl;
            break;
        case (CLInvalidNonNegativeInteger):
            std::cout << "ERROR: " << option << " requires an interger greater "
                      << "than or equal to 0." << std::endl;
            break;
        case (CLInvalidFloat):
            std::cout << "ERROR: " << option << " requires a number as input." << std::endl;
            break;
        case (CLInvalidPositiveFloat):
            std::cout << "ERROR: " << option << " must be greater than 0 (eg. .0001)." << std::endl;
            break;
        case (CLInvalidNonNegativeFloat):
            std::cout << "ERROR: " << option << " must be greater than or equal to 0." << std::endl;
            break;
        default:
            break;
    };
}

CommandLineParser::ErrorValue CommandLineParser::ParseFlag(int optionValueIndex)
{

    *boolValues[optionValueIndex] = !(*boolValues[optionValueIndex]);
    return CLGood;
}

CommandLineParser::ErrorValue CommandLineParser::ParseInteger(int optionValueIndex, int &argi,
                                                              int argc, char *argv[])
{

    if (argi >= argc) {
        --argi;
        return CLMissingValue;
    }
    if (IsInteger(argv[argi])) {
        *intValues[optionValueIndex] = atoi(argv[argi]);
        ++argi;
        return CLGood;
    } else {
        // reset argi to the flag that was broken.
        --argi;
        return CLInvalidInteger;
    }
}

CommandLineParser::ErrorValue CommandLineParser::ParsePositiveInteger(int optionValueIndex,
                                                                      int &argi, int argc,
                                                                      char *argv[])
{

    int value;
    if (argi >= argc) {
        --argi;
        return CLMissingValue;
    }
    if (IsInteger(argv[argi])) {
        value = atoi(argv[argi]);
        if (value > 0) {
            *intValues[optionValueIndex] = value;
            ++argi;
            return CLGood;
        }
    }
    // reset argi to the flag that was broken.
    --argi;
    return CLInvalidPositiveInteger;
}

CommandLineParser::ErrorValue CommandLineParser::ParseNonNegativeInteger(int optionValueIndex,
                                                                         int &argi, int argc,
                                                                         char *argv[])
{

    int value;
    if (argi >= argc) {
        --argi;
        return CLMissingValue;
    }
    if (IsInteger(argv[argi])) {
        value = atoi(argv[argi]);
        if (value >= 0) {
            *intValues[optionValueIndex] = value;
            ++argi;
            return CLGood;
        }
    }
    // reset argi to the flag that was broken.
    --argi;
    return CLInvalidNonNegativeInteger;
}

CommandLineParser::ErrorValue CommandLineParser::ParseFloat(int optionValueIndex, int &argi,
                                                            int argc, char *argv[])
{

    if (argi >= argc) {
        --argi;
        return CLMissingValue;
    }
    if (IsFloat(argv[argi])) {
        *floatValues[optionValueIndex] = atof(argv[argi]);
        ++argi;
        return CLGood;
    } else {
        // reset argi to the flag that was broken.
        --argi;
        return CLInvalidFloat;
    }
}

CommandLineParser::ErrorValue CommandLineParser::ParsePositiveFloat(int optionValueIndex, int &argi,
                                                                    int argc, char *argv[])
{

    float value;
    if (argi >= argc) {
        --argi;
        return CLMissingValue;
    }
    if (IsFloat(argv[argi])) {
        value = atof(argv[argi]);
        if (value > 0) {
            *floatValues[optionValueIndex] = value;
            ++argi;
            return CLGood;
        }
    }
    // reset argi pointer to bad flag
    --argi;
    return CLInvalidPositiveFloat;
}

CommandLineParser::ErrorValue CommandLineParser::ParseNonNegativeFloat(int optionValueIndex,
                                                                       int &argi, int argc,
                                                                       char *argv[])
{

    float value;
    if (argi >= argc) {
        --argi;
        return CLMissingValue;
    }
    if (IsFloat(argv[argi])) {
        value = atof(argv[argi]);
        if (value >= 0) {
            *floatValues[optionValueIndex] = value;
            ++argi;
            return CLGood;
        }
    }
    // reset argi to the flag that was broken.
    --argi;
    return CLInvalidNonNegativeFloat;
}

CommandLineParser::ErrorValue CommandLineParser::ParseString(int optionValueIndex, int &argi,
                                                             int argc, char *argv[])
{

    if (argi >= argc) {
        --argi;
        return CLMissingValue;
    }
    if (argi < argc) {
        *stringValues[optionValueIndex] = argv[argi];
        ++argi;
        return CLGood;
    } else {
        // reset argi to the flag that was broken.
        --argi;
        return CLMissingValue;
    }
}

bool CommandLineParser::IsValuedOption(OptionType optType)
{
    if (optType == Integer or optType == PositiveInteger or optType == NonNegativeInteger or
        optType == Float or optType == PositiveFloat or optType == NonNegativeFloat or
        optType == String or optType == StringList) {
        return true;
    } else {
        return false;
    }
}

CommandLineParser::ErrorValue CommandLineParser::ParseIntList(int optionValueIndex, int &argi,
                                                              int argc, char *argv[])
{

    if (argi >= argc) {
        --argi;
        return CLMissingValue;
    }
    ErrorValue ev;
    ev = CLMissingValue;
    while (argi < argc and !IsOption(argv[argi])) {
        if (IsInteger(argv[argi])) {
            intListValues[optionValueIndex]->push_back(atoi(argv[argi]));
            ++argi;
            ev = CLGood;
        } else {
            ev = CLInvalidInteger;
            --argi;
            break;
        }
    }
    if (ev == CLMissingValue) {
        // reset argi to the flag that was broken.
        --argi;
    }
    return ev;
}

CommandLineParser::ErrorValue CommandLineParser::ParseStringList(int optionValueIndex, int &argi,
                                                                 int argc, char *argv[])
{

    if (argi >= argc) {
        --argi;
        return CLMissingValue;
    }
    ErrorValue ev;
    ev = CLMissingValue;
    while (argi < argc and !IsOption(argv[argi])) {
        stringListValues[optionValueIndex]->push_back(argv[argi]);
        ++argi;
        ev = CLGood;
    }
    if (ev == CLMissingValue) {
        // reset argi to the flag that was broken.
        --argi;
    }
    return ev;
}

void CommandLineParser::PrintVersion() { std::cout << programName << "\t" << version << std::endl; }

void CommandLineParser::PrintUsage()
{
    std::ios::fmtflags f = std::cout.flags();
    if (helpString != "") {
        std::cout << helpString << std::endl;
        return;
    } else {
        if (programSummary.size() > 0) {
            std::cout << programName << " ";

            PrintIndentedText(std::cout, programSummary, programName.size(), lineLength);

            std::cout << std::endl;
        }
        std::cout << std::endl << "usage: " << programName;
        size_t i = 0;
        int maxOptionLength = GetMaxOptionLength();
        while (i < optionList.size() and named[i] == false) {
            std::cout << " ";
            if (optionRequired[i] == false) {
                std::cout << "[";
            }
            std::cout << optionList[i];
            if (optionRequired[i] == false) {
                std::cout << "]";
            }
            i++;
        }
        if (i < optionList.size()) {
            std::cout << " [options] ";
        }
        std::cout << std::endl << std::endl;
        i = 0;
        while (i < optionList.size() and named[i] == false) {
            if (!named[i]) {
                std::cout << "   " << std::setw(maxOptionLength) << std::left << optionList[i]
                          << std::endl;

                PrintIndentedText(std::cout, descriptions[i], 15, (int)lineLength, 15);

                std::cout << std::endl;
            }
            i++;
        }
        for (; i < optionList.size(); i++) {
            std::string wholeName = "-";
            wholeName += optionList[i];
            if (IsValuedOption(optionTypeList[i])) {
                wholeName += " value ";
            }
            std::cout << "  " << std::setw(maxOptionLength) << std::left << wholeName << std::endl;

            PrintIndentedText(std::cout, descriptions[i], 15, (int)lineLength, 15);

            std::cout << std::endl;
        }
    }
    if (examples.size() > 0) {
        std::cout << std::endl << std::endl;
        PrintIndentedText(std::cout, examples, 5, (int)lineLength, 5);
        std::cout << std::endl;
    }
    std::cout.flags(f);
}

int CommandLineParser::GetNextWordLength(std::string &text, int pos)
{
    int startPos = pos;
    int textLength = text.size();
    while (pos < textLength and (!IsWhitespace(text[pos]))) {
        pos++;
    }
    return pos - startPos;
}

void CommandLineParser::PrintIndentedText(std::ostream &out, std::string &text, int allLineIndent,
                                          int lineLength, int firstLineIndent)
{

    int textPos = 0;
    std::vector<std::string> words;
    ToWords(text, words);
    int curLineLength;
    int i;
    if (firstLineIndent == 0) {
        curLineLength = allLineIndent;
    } else {
        for (i = 0; i < firstLineIndent; i++) {
            out << " ";
        }
        curLineLength = allLineIndent;
    }
    std::string indentation;
    indentation.insert(0, allLineIndent, ' ');
    int textLength = text.size();
    while (textPos < textLength) {
        // Print some whitespace
        while (textPos < textLength and curLineLength < lineLength and
               IsWhitespace(text[textPos])) {
            out << text[textPos];
            // Some extra logic in case
            if (text[textPos] == '\n') {
                // an extra line was printed, so skip to the next line.
                curLineLength = lineLength + 1;
                // done printing this line.
                curLineLength = 0;
                if (textPos < textLength) {
                    out << indentation;
                    curLineLength = allLineIndent;
                }
            } else {
                curLineLength++;
                if (curLineLength == lineLength) {
                    std::cout << std::endl;
                    curLineLength = 0;
                    if (textPos < textLength) {
                        out << indentation;
                        curLineLength = allLineIndent;
                    }
                }
            }
            textPos++;
        }
        // Possibly print a word.
        if (!IsWhitespace(text[textPos])) {
            int nextWordLength = GetNextWordLength(text, textPos);
            if (nextWordLength + curLineLength >= lineLength) {
                //
                // The next word runs past the end of this line,
                // print it on a newline, or print part of it on
                // this line if the whole word wraps.
                //
                if (nextWordLength > lineLength) {
                    // This word will never fit on a line, print part of it.
                    for (; curLineLength < lineLength; curLineLength++, textPos++) {
                        out << text[textPos];
                    }
                }
                out << std::endl;
                out << indentation;
                curLineLength = allLineIndent;
            } else {
                int i;
                for (i = 0; i < nextWordLength; i++, textPos++, curLineLength++) {
                    out << text[textPos];
                }
            }
        }
    }
}

unsigned int CommandLineParser::GetMaxOptionLength()
{
    unsigned int maxLength = 0;
    for (size_t i = 0; i < optionList.size(); i++) {
        if (optionList[i].size() > maxLength) maxLength = optionList[i].size();
    }
    return maxLength;
}

CommandLineParser::ErrorValue CommandLineParser::PrintErrorOnMissingOptions()
{
    ErrorValue ev = CLGood;
    for (size_t i = 0; i < optionList.size(); i++) {
        if (optionRequired[i] and !optionUsed[i]) {
            std::cout << "ERROR, the option " << optionList[i] << " must be specified."
                      << std::endl;
            ev = CLMissingOption;
        }
    }
    return ev;
}
