#include "clicommandsyntax.h"
#include "commands/clicommand.h"
#include "commands/clicommandfactory.h"
#include "cli.h"
#include <QDebug>

CliCommandSyntax::CliCommandSyntax()
{
}

CliCommandSyntax::~CliCommandSyntax()
{
    for (Argument* arg : arguments)
        delete arg;

    arguments.clear();
    argumentMap.clear();

    for (Option* opt : options)
        delete opt;

    optionMap.clear();
    optionsLongNameMap.clear();
    optionsShortNameMap.clear();
    options.clear();
}

void CliCommandSyntax::addArgument(int id, const QString& name, bool mandatory)
{
    addArgumentInternal(id, {name}, mandatory, Argument::REGULAR);
}

void CliCommandSyntax::addStrictArgument(int id, const QStringList& values, bool mandatory)
{
    addArgumentInternal(id, values, mandatory, Argument::STRICT);
}

void CliCommandSyntax::addAlternatedArgument(int id, const QStringList& names, bool mandatory)
{
    addArgumentInternal(id, names, mandatory, Argument::ALTERNATED);
}

void CliCommandSyntax::addOptionShort(int id, const QString& shortName)
{
    addOptionInternal(id, shortName, QString(), QString());
}

void CliCommandSyntax::addOptionLong(int id, const QString& longName)
{
    addOptionInternal(id, QString(), longName, QString());
}

void CliCommandSyntax::addOption(int id, const QString& shortName, const QString& longName)
{
    addOptionInternal(id, shortName, longName, QString());
}

void CliCommandSyntax::addOptionWithArgShort(int id, const QString& shortName, const QString& argName)
{
    addOptionInternal(id, shortName, QString(), argName);
}

void CliCommandSyntax::addOptionWithArgLong(int id, const QString& longName, const QString& argName)
{
    addOptionInternal(id, QString(), longName, argName);
}

void CliCommandSyntax::addOptionWithArg(int id, const QString& shortName, const QString& longName, const QString& argName)
{
    addOptionInternal(id, shortName, longName, argName);
}

CliCommandSyntax::Argument* CliCommandSyntax::addArgumentInternal(int id, const QStringList& names, bool mandatory, Argument::Type type)
{
    checkNewArgument(mandatory);

    Argument* arg = new Argument;
    arg->mandatory = mandatory;
    arg->id = id;
    arg->type = type;
    arg->names = names;
    arguments << arg;
    argumentMap[id] = arg;
    return arg;
}

CliCommandSyntax::Option* CliCommandSyntax::addOptionInternal(int id, const QString& shortName, const QString& longName, const QString& argName)
{
    Option* opt = new Option;
    opt->shortName = shortName;
    opt->longName = longName;
    opt->id = id;
    opt->argName = argName;
    optionMap[id] = opt;
    options << opt;
    optionsShortNameMap[shortName] = opt;
    optionsLongNameMap[longName] = opt;
    return opt;
}

bool CliCommandSyntax::getStrictArgumentCount() const
{
    return strictArgumentCount;
}

void CliCommandSyntax::setStrictArgumentCount(bool value)
{
    strictArgumentCount = value;
}

bool CliCommandSyntax::parse(const QStringList& args)
{
    pastOptions = (options.size() == 0);
    lastParsedOption = nullptr;
    QString arg;
    bool res = false;
    int argCnt = args.size();
    for (int argIdx = 0; argIdx < argCnt; argIdx++)
    {
        arg = args[argIdx];

        if (arg == "--help")
        {
            CliCommand* help = CliCommandFactory::getCommand("help");
            help->setup(CLI::getInstance());
            help->parseArgs({getName()});
            help->execute();
            return false;
        }
        else if (pastOptions)
        {
            res = parseArg(arg);
        }
        else if (arg == "--")
        {
            pastOptions = true;
            res = true;
        }
        else if (arg.startsWith("-"))
        {
            res = parseOpt(arg, args, argIdx);
        }
        else
        {
            pastOptions = true;
            res = parseArg(arg);
        }

        if (!res)
            return false;
    }

    if (strictArgumentCount && argPosition < requiredArguments())
    {
        parsingErrorText = QObject::tr("Insufficient number of arguments.");
        return false;
    }

    return true;
}

QString CliCommandSyntax::getErrorText() const
{
    return parsingErrorText;
}

QStringList CliCommandSyntax::getStrictArgumentCandidates()
{
    QStringList results;
    if (!pastOptions)
    {
        if (lastParsedOption && !lastParsedOption->argName.isEmpty())
            return results; // this case is covered by getRegularArgumentCandidates()

        for (Option* opt : options)
        {
            if (opt->requested)
                continue;

            if (!opt->shortName.isEmpty())
                results << "-"+opt->shortName;

            if (!opt->longName.isEmpty())
                results << "--"+opt->longName;
        }
        results << "--";
    }

    if (argPosition < arguments.size() && arguments[argPosition]->type == Argument::STRICT)
        results += arguments[argPosition]->names;

    return results;
}

QList<int> CliCommandSyntax::getRegularArgumentCandidates()
{
    QList<int> results;

    if (!pastOptions && lastParsedOption && !lastParsedOption->argName.isEmpty())
    {
        // We're exactly at the spot where the option argument is expected
        results << lastParsedOption->id;
        return results;
    }

    if (argPosition < arguments.size())
    {
        switch (arguments[argPosition]->type)
        {
            case Argument::REGULAR:
            case Argument::ALTERNATED:
                results << arguments[argPosition]->id;
                break;
            case Argument::STRICT:
                break;
        }
    }

    return results;
}

void CliCommandSyntax::addAlias(const QString& alias)
{
    aliases << alias;
}

QStringList CliCommandSyntax::getAliases() const
{
    return aliases;
}

QString CliCommandSyntax::getSyntaxDefinition() const
{
    return getSyntaxDefinition(name);
}

QString CliCommandSyntax::getSyntaxDefinition(const QString& usedName) const
{
    static const QString mandatoryArgTempl = "<%1>";
    static const QString optionalArgTempl = "[<%1>]";
    static const QString mandatoryStrictTempl = "%1";
    static const QString optionalStrictTempl = "[%1]";
    static const QString optionTempl = "[%1]";
    static const QString optionWithArgTempl = "[%1 <%2>]";

    QStringList words;
    words << usedName;

    QString optName;
    QStringList optNameParts;
    for (Option* opt : options)
    {
        optNameParts.clear();;
        if (!opt->shortName.isEmpty())
            optNameParts << "-"+opt->shortName;

        if (!opt->longName.isEmpty())
            optNameParts += "--"+opt->longName;

        optName = optNameParts.join("|");

        words << (opt->argName.isEmpty() ? optionTempl.arg(optName) : optionWithArgTempl.arg(optName).arg(opt->argName));
    }

    QString templ;
    QString argName;
    for (Argument* arg : arguments)
    {
        templ = (arg->mandatory ? mandatoryArgTempl : optionalArgTempl);
        switch (arg->type)
        {
            case CliCommandSyntax::Argument::ALTERNATED:
                argName = arg->names.join("|");
                break;
            case CliCommandSyntax::Argument::REGULAR:
                argName = arg->names.first();
                break;
            case CliCommandSyntax::Argument::STRICT:
                argName = arg->names.join("|");
                templ = (arg->mandatory ? mandatoryStrictTempl : optionalStrictTempl);
                break;
        }
        words << templ.arg(argName);
    }

    return words.join(" ");
}

bool CliCommandSyntax::isArgumentSet(int id) const
{
    if (!argumentMap.contains(id))
        return false;

    return argumentMap[id]->defined;
}

QString CliCommandSyntax::getArgument(int id) const
{
    if (!argumentMap.contains(id))
        return QString();

    return argumentMap[id]->value;
}

bool CliCommandSyntax::isOptionSet(int id) const
{
    if (!optionMap.contains(id))
        return false;

    return optionMap[id]->requested;
}

QString CliCommandSyntax::getOptionValue(int id) const
{
    if (!optionMap.contains(id))
        return QString();

    return optionMap[id]->value;
}

bool CliCommandSyntax::parseArg(const QString& arg)
{
    if (strictArgumentCount && arguments.size() < (argPosition + 1))
    {
        parsingErrorText = QObject::tr("Too many arguments.");
        return false;
    }

    switch (arguments[argPosition]->type)
    {
        case CliCommandSyntax::Argument::ALTERNATED:
        {
            arguments[argPosition]->value = arg;
            arguments[argPosition]->defined = true;
            break;
        }
        case CliCommandSyntax::Argument::REGULAR:
        {
            arguments[argPosition]->value = arg;
            arguments[argPosition]->defined = true;
            break;
        }
        case CliCommandSyntax::Argument::STRICT:
        {
            if (!arguments[argPosition]->names.contains(arg))
            {
                parsingErrorText = QObject::tr("Invalid argument value: %1.\nExpected one of: %2").arg(arg)
                        .arg(arguments[argPosition]->names.join(", "));

                return false;
            }
            arguments[argPosition]->value = arg;
            arguments[argPosition]->defined = true;
            break;
        }
        default:
            qCritical() << "Invalid argument type in CliCommandSyntax:" << arguments[argPosition]->type;
            return false;
    }

    argPosition++;
    return true;
}

bool CliCommandSyntax::parseOpt(const QString& arg, const QStringList& args, int& argIdx)
{
    Option* opt = nullptr;
    if (arg.startsWith("--"))
    {
        QString longName = arg.mid(2);
        if (optionsLongNameMap.contains(longName))
            opt = optionsLongNameMap.value(longName);
    }
    else
    {
        QString shortName = arg.mid(1);
        if (optionsShortNameMap.contains(shortName))
            opt = optionsShortNameMap.value(shortName);
    }

    if (!opt)
    {
        parsingErrorText = QObject::tr("Unknown option: %1", "CLI command syntax").arg(arg);
        return false;
    }

    opt->requested = true;
    lastParsedOption = opt;

    if (!opt->argName.isEmpty())
    {
        if (args.size() <= (argIdx + 1))
        {
            parsingErrorText = QObject::tr("Option %1 requires an argument.", "CLI command syntax").arg(arg);
            return false;
        }

        argIdx++;
        opt->value = args[argIdx];
    }
    return true;
}

int CliCommandSyntax::requiredArguments() const
{
    int cnt = 0;
    for (Argument* arg : arguments)
    {
        if (arg->mandatory)
            cnt++;
    }
    return cnt;
}

void CliCommandSyntax::checkNewArgument(bool mandatory)
{
    if (arguments.size() > 0 && !arguments.last()->mandatory && mandatory)
    {
        qWarning() << "Adding mandatory CLI command argument after optional argument. This will result in invalid syntax definition. The command is:"
                   << this->name;
    }
}

QString CliCommandSyntax::getName() const
{
    return name;
}

void CliCommandSyntax::setName(const QString& value)
{
    name = value;
}

