/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 * vim: set ts=8 sts=4 et sw=4 tw=99:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "shell/jsoptparse.h"

#include <ctype.h>
#include <stdarg.h>

#include "jsutil.h"

using namespace js;
using namespace js::cli;
using namespace js::cli::detail;

const char OptionParser::prognameMeta[] = "{progname}";

#define OPTION_CONVERT_IMPL(__cls) \
    bool \
    Option::is##__cls##Option() const \
    { \
        return kind == OptionKind##__cls; \
    } \
    __cls##Option * \
    Option::as##__cls##Option() \
    { \
        MOZ_ASSERT(is##__cls##Option()); \
        return static_cast<__cls##Option*>(this); \
    } \
    const __cls##Option * \
    Option::as##__cls##Option() const \
    { \
        return const_cast<Option*>(this)->as##__cls##Option(); \
    }

ValuedOption*
Option::asValued()
{
    MOZ_ASSERT(isValued());
    return static_cast<ValuedOption*>(this);
}

const ValuedOption*
Option::asValued() const
{
    return const_cast<Option*>(this)->asValued();
}

OPTION_CONVERT_IMPL(Bool)
OPTION_CONVERT_IMPL(String)
OPTION_CONVERT_IMPL(Int)
OPTION_CONVERT_IMPL(MultiString)

void
OptionParser::setArgTerminatesOptions(const char* name, bool enabled)
{
    findArgument(name)->setTerminatesOptions(enabled);
}

void
OptionParser::setArgCapturesRest(const char* name)
{
    MOZ_ASSERT(restArgument == -1, "only one argument may be set to capture the rest");
    restArgument = findArgumentIndex(name);
    MOZ_ASSERT(restArgument != -1, "unknown argument name passed to setArgCapturesRest");
}

OptionParser::Result
OptionParser::error(const char* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    fprintf(stderr, "Error: ");
    vfprintf(stderr, fmt, args);
    va_end(args);
    fputs("\n\n", stderr);
    return ParseError;
}

/* Quick and dirty paragraph printer. */
static void
PrintParagraph(const char* text, unsigned startColno, const unsigned limitColno, bool padFirstLine)
{
    unsigned colno = startColno;
    unsigned indent = 0;
    const char* it = text;

    if (padFirstLine)
        printf("%*s", startColno, "");

    /* Skip any leading spaces. */
    while (*it != '\0' && isspace(*it))
        ++it;

    while (*it != '\0') {
        MOZ_ASSERT(!isspace(*it) || *it == '\n');

        /* Delimit the current token. */
        const char* limit = it;
        while (!isspace(*limit) && *limit != '\0')
            ++limit;

        /*
         * If the current token is longer than the available number of columns,
         * then make a line break before printing the token.
         */
        size_t tokLen = limit - it;
        if (tokLen + colno >= limitColno) {
            printf("\n%*s%.*s", startColno + indent, "", int(tokLen), it);
            colno = startColno + tokLen;
        } else {
            printf("%.*s", int(tokLen), it);
            colno += tokLen;
        }

        switch (*limit) {
          case '\0':
            return;
          case ' ':
            putchar(' ');
            colno += 1;
            it = limit;
            while (*it == ' ')
                ++it;
            break;
          case '\n':
            /* |text| wants to force a newline here. */
            printf("\n%*s", startColno, "");
            colno = startColno;
            it = limit + 1;
            /* Could also have line-leading spaces. */
            indent = 0;
            while (*it == ' ') {
                putchar(' ');
                ++colno;
                ++indent;
                ++it;
            }
            break;
          default:
            MOZ_CRASH("unhandled token splitting character in text");
        }
    }
}

static const char*
OptionFlagsToFormatInfo(char shortflag, bool isValued, size_t* length)
{
    static const char * const fmt[4] = { "  -%c --%s ",
                                         "  --%s ",
                                         "  -%c --%s=%s ",
                                         "  --%s=%s " };

    /* How mny chars w/o longflag? */
    size_t lengths[4] = { strlen(fmt[0]) - 3,
                          strlen(fmt[1]) - 3,
                          strlen(fmt[2]) - 5,
                          strlen(fmt[3]) - 5 };
    int index = isValued ? 2 : 0;
    if (!shortflag)
        index++;

    *length = lengths[index];
    return fmt[index];
}

OptionParser::Result
OptionParser::printHelp(const char* progname)
{
    const char* prefixEnd = strstr(usage, prognameMeta);
    if (prefixEnd) {
        printf("%.*s%s%s\n", int(prefixEnd - usage), usage, progname,
               prefixEnd + sizeof(prognameMeta) - 1);
    } else {
        puts(usage);
    }

    if (descr) {
        putchar('\n');
        PrintParagraph(descr, 2, descrWidth, true);
        putchar('\n');
    }

    if (version)
        printf("\nVersion: %s\n\n", version);

    if (!arguments.empty()) {
        printf("Arguments:\n");

        static const char fmt[] = "  %s ";
        size_t fmtChars = sizeof(fmt) - 2;
        size_t lhsLen = 0;
        for (Option* arg : arguments)
            lhsLen = Max(lhsLen, strlen(arg->longflag) + fmtChars);

        for (Option* arg : arguments) {
            size_t chars = printf(fmt, arg->longflag);
            for (; chars < lhsLen; ++chars)
                putchar(' ');
            PrintParagraph(arg->help, lhsLen, helpWidth, false);
            putchar('\n');
        }
        putchar('\n');
    }

    if (!options.empty()) {
        printf("Options:\n");

        /* Calculate sizes for column alignment. */
        size_t lhsLen = 0;
        for (Option* opt : options) {
            size_t longflagLen = strlen(opt->longflag);

            size_t fmtLen;
            OptionFlagsToFormatInfo(opt->shortflag, opt->isValued(), &fmtLen);

            size_t len = fmtLen + longflagLen;
            if (opt->isValued())
                len += strlen(opt->asValued()->metavar);
            lhsLen = Max(lhsLen, len);
        }

        /* Print option help text. */
        for (Option* opt : options) {
            size_t fmtLen;
            const char* fmt = OptionFlagsToFormatInfo(opt->shortflag, opt->isValued(), &fmtLen);
            size_t chars;
            if (opt->isValued()) {
                if (opt->shortflag)
                    chars = printf(fmt, opt->shortflag, opt->longflag, opt->asValued()->metavar);
                else
                    chars = printf(fmt, opt->longflag, opt->asValued()->metavar);
            } else {
                if (opt->shortflag)
                    chars = printf(fmt, opt->shortflag, opt->longflag);
                else
                    chars = printf(fmt, opt->longflag);
            }
            for (; chars < lhsLen; ++chars)
                putchar(' ');
            PrintParagraph(opt->help, lhsLen, helpWidth, false);
            putchar('\n');
        }
    }

    return EarlyExit;
}

OptionParser::Result
OptionParser::printVersion()
{
    MOZ_ASSERT(version);
    printf("%s\n", version);
    return EarlyExit;
}

OptionParser::Result
OptionParser::extractValue(size_t argc, char** argv, size_t* i, char** value)
{
    MOZ_ASSERT(*i < argc);
    char* eq = strchr(argv[*i], '=');
    if (eq) {
        *value = eq + 1;
        if (*value[0] == '\0')
            return error("A value is required for option %.*s", (int) (eq - argv[*i]), argv[*i]);
        return Okay;
    }

    if (argc == *i + 1)
        return error("Expected a value for option %s", argv[*i]);

    *i += 1;
    *value = argv[*i];
    return Okay;
}

OptionParser::Result
OptionParser::handleOption(Option* opt, size_t argc, char** argv, size_t* i, bool* optionsAllowed)
{
    if (opt->getTerminatesOptions())
        *optionsAllowed = false;

    switch (opt->kind) {
      case OptionKindBool:
      {
        if (opt == &helpOption)
            return printHelp(argv[0]);
        if (opt == &versionOption)
            return printVersion();
        opt->asBoolOption()->value = true;
        return Okay;
      }
      /*
       * Valued options are allowed to specify their values either via
       * successive arguments or a single --longflag=value argument.
       */
      case OptionKindString:
      {
        char* value = nullptr;
        if (Result r = extractValue(argc, argv, i, &value))
            return r;
        opt->asStringOption()->value = value;
        return Okay;
      }
      case OptionKindInt:
      {
        char* value = nullptr;
        if (Result r = extractValue(argc, argv, i, &value))
            return r;
        opt->asIntOption()->value = atoi(value);
        return Okay;
      }
      case OptionKindMultiString:
      {
        char* value = nullptr;
        if (Result r = extractValue(argc, argv, i, &value))
            return r;
        StringArg arg(value, *i);
        return opt->asMultiStringOption()->strings.append(arg) ? Okay : Fail;
      }
      default:
        MOZ_CRASH("unhandled option kind");
    }
}

OptionParser::Result
OptionParser::handleArg(size_t argc, char** argv, size_t* i, bool* optionsAllowed)
{
    if (nextArgument >= arguments.length())
        return error("Too many arguments provided");

    Option* arg = arguments[nextArgument];

    if (arg->getTerminatesOptions())
        *optionsAllowed = false;

    switch (arg->kind) {
      case OptionKindString:
        arg->asStringOption()->value = argv[*i];
        nextArgument += 1;
        return Okay;
      case OptionKindMultiString:
      {
        /* Don't advance the next argument -- there can only be one (final) variadic argument. */
        StringArg value(argv[*i], *i);
        return arg->asMultiStringOption()->strings.append(value) ? Okay : Fail;
      }
      default:
        MOZ_CRASH("unhandled argument kind");
    }
}

OptionParser::Result
OptionParser::parseArgs(int inputArgc, char** argv)
{
    MOZ_ASSERT(inputArgc >= 0);
    size_t argc = inputArgc;
    /* Permit a "no more options" capability, like |--| offers in many shell interfaces. */
    bool optionsAllowed = true;

    for (size_t i = 1; i < argc; ++i) {
        char* arg = argv[i];
        Result r;
        /* Note: solo dash option is actually a 'stdin' argument. */
        if (arg[0] == '-' && arg[1] != '\0' && optionsAllowed) {
            /* Option. */
            Option* opt;
            if (arg[1] == '-') {
                if (arg[2] == '\0') {
                    /* End of options */
                    optionsAllowed = false;
                    nextArgument = restArgument;
                    continue;
                } else {
                    /* Long option. */
                    opt = findOption(arg + 2);
                    if (!opt)
                        return error("Invalid long option: %s", arg);
                }
            } else {
                /* Short option */
                if (arg[2] != '\0')
                    return error("Short option followed by junk: %s", arg);
                opt = findOption(arg[1]);
                if (!opt)
                    return error("Invalid short option: %s", arg);
            }

            r = handleOption(opt, argc, argv, &i, &optionsAllowed);
        } else {
            /* Argument. */
            r = handleArg(argc, argv, &i, &optionsAllowed);
        }

        if (r != Okay)
            return r;
    }
    return Okay;
}

void
OptionParser::setHelpOption(char shortflag, const char* longflag, const char* help)
{
    helpOption.setFlagInfo(shortflag, longflag, help);
}

bool
OptionParser::getHelpOption() const
{
    return helpOption.value;
}

bool
OptionParser::getBoolOption(char shortflag) const
{
    return findOption(shortflag)->asBoolOption()->value;
}

int
OptionParser::getIntOption(char shortflag) const
{
    return findOption(shortflag)->asIntOption()->value;
}

const char*
OptionParser::getStringOption(char shortflag) const
{
    return findOption(shortflag)->asStringOption()->value;
}

MultiStringRange
OptionParser::getMultiStringOption(char shortflag) const
{
    const MultiStringOption* mso = findOption(shortflag)->asMultiStringOption();
    return MultiStringRange(mso->strings.begin(), mso->strings.end());
}

bool
OptionParser::getBoolOption(const char* longflag) const
{
    return findOption(longflag)->asBoolOption()->value;
}

int
OptionParser::getIntOption(const char* longflag) const
{
    return findOption(longflag)->asIntOption()->value;
}

const char*
OptionParser::getStringOption(const char* longflag) const
{
    return findOption(longflag)->asStringOption()->value;
}

MultiStringRange
OptionParser::getMultiStringOption(const char* longflag) const
{
    const MultiStringOption* mso = findOption(longflag)->asMultiStringOption();
    return MultiStringRange(mso->strings.begin(), mso->strings.end());
}

OptionParser::~OptionParser()
{
    for (Option* opt : options)
        js_delete<Option>(opt);
    for (Option* arg : arguments)
        js_delete<Option>(arg);
}

Option*
OptionParser::findOption(char shortflag)
{
    for (Option* opt : options) {
        if (opt->shortflag == shortflag)
            return opt;
    }

    if (versionOption.shortflag == shortflag)
        return &versionOption;

    return helpOption.shortflag == shortflag ? &helpOption : nullptr;
}

const Option*
OptionParser::findOption(char shortflag) const
{
    return const_cast<OptionParser*>(this)->findOption(shortflag);
}

Option*
OptionParser::findOption(const char* longflag)
{
    for (Option* opt : options) {
        const char* target = opt->longflag;
        if (opt->isValued()) {
            size_t targetLen = strlen(target);
            /* Permit a trailing equals sign on the longflag argument. */
            for (size_t i = 0; i < targetLen; ++i) {
                if (longflag[i] == '\0' || longflag[i] != target[i])
                    goto no_match;
            }
            if (longflag[targetLen] == '\0' || longflag[targetLen] == '=')
                return opt;
        } else {
            if (strcmp(target, longflag) == 0)
                return opt;
        }
  no_match:;
    }

    if (strcmp(versionOption.longflag, longflag) == 0)
        return &versionOption;

    return strcmp(helpOption.longflag, longflag) ? nullptr : &helpOption;
}

const Option*
OptionParser::findOption(const char* longflag) const
{
    return const_cast<OptionParser*>(this)->findOption(longflag);
}

/* Argument accessors */

int
OptionParser::findArgumentIndex(const char* name) const
{
    for (Option * const* it = arguments.begin(); it != arguments.end(); ++it) {
        const char* target = (*it)->longflag;
        if (strcmp(target, name) == 0)
            return it - arguments.begin();
    }
    return -1;
}

Option*
OptionParser::findArgument(const char* name)
{
    int index = findArgumentIndex(name);
    return (index == -1) ? nullptr : arguments[index];
}

const Option*
OptionParser::findArgument(const char* name) const
{
    int index = findArgumentIndex(name);
    return (index == -1) ? nullptr : arguments[index];
}

const char*
OptionParser::getStringArg(const char* name) const
{
    return findArgument(name)->asStringOption()->value;
}

MultiStringRange
OptionParser::getMultiStringArg(const char* name) const
{
    const MultiStringOption* mso = findArgument(name)->asMultiStringOption();
    return MultiStringRange(mso->strings.begin(), mso->strings.end());
}

/* Option builders */

bool
OptionParser::addIntOption(char shortflag, const char* longflag, const char* metavar,
                           const char* help, int defaultValue)
{
    if (!options.reserve(options.length() + 1))
        return false;
    IntOption* io = js_new<IntOption>(shortflag, longflag, help, metavar, defaultValue);
    if (!io)
        return false;
    options.infallibleAppend(io);
    return true;
}

bool
OptionParser::addBoolOption(char shortflag, const char* longflag, const char* help)
{
    if (!options.reserve(options.length() + 1))
        return false;
    BoolOption* bo = js_new<BoolOption>(shortflag, longflag, help);
    if (!bo)
        return false;
    options.infallibleAppend(bo);
    return true;
}

bool
OptionParser::addStringOption(char shortflag, const char* longflag, const char* metavar,
                              const char* help)
{
    if (!options.reserve(options.length() + 1))
        return false;
    StringOption* so = js_new<StringOption>(shortflag, longflag, help, metavar);
    if (!so)
        return false;
    options.infallibleAppend(so);
    return true;
}

bool
OptionParser::addMultiStringOption(char shortflag, const char* longflag, const char* metavar,
                                   const char* help)
{
    if (!options.reserve(options.length() + 1))
        return false;
    MultiStringOption* mso = js_new<MultiStringOption>(shortflag, longflag, help, metavar);
    if (!mso)
        return false;
    options.infallibleAppend(mso);
    return true;
}

/* Argument builders */

bool
OptionParser::addOptionalStringArg(const char* name, const char* help)
{
    if (!arguments.reserve(arguments.length() + 1))
        return false;
    StringOption* so = js_new<StringOption>(1, name, help, (const char*) nullptr);
    if (!so)
        return false;
    arguments.infallibleAppend(so);
    return true;
}

bool
OptionParser::addOptionalMultiStringArg(const char* name, const char* help)
{
    MOZ_ASSERT_IF(!arguments.empty(), !arguments.back()->isVariadic());
    if (!arguments.reserve(arguments.length() + 1))
        return false;
    MultiStringOption* mso = js_new<MultiStringOption>(1, name, help, (const char*) nullptr);
    if (!mso)
        return false;
    arguments.infallibleAppend(mso);
    return true;
}