// Copyright (C) 2017-2020 Ford Motor Company
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include <qcommandlineoption.h>
#include <qcommandlineparser.h>
#include <qcoreapplication.h>
#include <qfileinfo.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qjsonarray.h>

#include "cppcodegenerator.h"
#include "repcodegenerator.h"
#include "repparser.h"
#include "utils.h"

#include <cstdio>

#define PROGRAM_NAME  "repc"
#define REPC_VERSION  "1.0.0"

enum Mode {
    InRep = 1,
    InJson = 2,
    OutRep = 4,
    OutSource = 8,
    OutReplica = 16,
    OutMerged = OutSource | OutReplica
};

static const QLatin1String REP("rep");

QT_USE_NAMESPACE

int main(int argc, char **argv)
{
    QCoreApplication app(argc, argv);
    QCoreApplication::setApplicationVersion(QString::fromLatin1(REPC_VERSION));

    QString outputFile;
    QString inputFile;
    int mode = 0;
    QCommandLineParser parser;
    parser.setApplicationDescription(QStringLiteral("repc tool v%1 (Qt %2).\n")
                                     .arg(QStringLiteral(REPC_VERSION), QString::fromLatin1(QT_VERSION_STR)));
    parser.addHelpOption();
    parser.addVersionOption();
    QCommandLineOption inputTypeOption(QStringLiteral("i"));
    inputTypeOption.setDescription(QLatin1String("Input file type:\n"
                                                  "rep: replicant template files.\n"
                                                  "json: JSON output from moc of a Qt header file."));
    inputTypeOption.setValueName(QStringLiteral("rep|json"));
    parser.addOption(inputTypeOption);

    QCommandLineOption outputTypeOption(QStringLiteral("o"));
    outputTypeOption.setDescription(QLatin1String("Output file type:\n"
                                                   "source: generates source header. Is incompatible with \"-i src\" option.\n"
                                                   "replica: generates replica header.\n"
                                                   "merged: generates combined replica/source header.\n"
                                                   "rep: generates replicant template file from C++ QOject classes. Is not compatible with \"-i rep\" option."));
    outputTypeOption.setValueName(QStringLiteral("source|replica|merged|rep"));
    parser.addOption(outputTypeOption);

    QCommandLineOption includePathOption(QStringLiteral("I"));
    includePathOption.setDescription(QStringLiteral("Add dir to the include path for header files. This parameter is needed only if the input file type is src (.h file)."));
    includePathOption.setValueName(QStringLiteral("dir"));
    parser.addOption(includePathOption);

    QCommandLineOption alwaysClassOption(QStringLiteral("c"));
    alwaysClassOption.setDescription(QStringLiteral("Always output `class` type for .rep files and never `POD`."));
    parser.addOption(alwaysClassOption);

    QCommandLineOption debugOption(QStringLiteral("d"));
    debugOption.setDescription(QStringLiteral("Print out parsing debug information (for troubleshooting)."));
    parser.addOption(debugOption);

    parser.addPositionalArgument(QStringLiteral("[json-file/rep-file]"),
            QStringLiteral("Input json/rep file to read from, otherwise stdin."));

    parser.addPositionalArgument(QStringLiteral("[rep-file/header-file]"),
            QStringLiteral("Output header/rep file to write to, otherwise stdout."));

    parser.process(app.arguments());

    const QStringList files = parser.positionalArguments();

    if (files.size() > 2) {
        fprintf(stderr, "%s", qPrintable(QLatin1String(PROGRAM_NAME ": Too many input, output files specified: '") + files.join(QStringLiteral("' '")) + QStringLiteral("\'.\n")));
        parser.showHelp(1);
    }

    if (parser.isSet(inputTypeOption)) {
        const QString &inputType = parser.value(inputTypeOption);
        if (inputType == REP)
            mode = InRep;
        else if (inputType == u"json")
            mode = InJson;
        else {
            fprintf(stderr, PROGRAM_NAME ": Unknown input type\"%s\".\n", qPrintable(inputType));
            parser.showHelp(1);
        }
    }

    if (parser.isSet(outputTypeOption)) {
        const QString &outputType = parser.value(outputTypeOption);
        if (outputType == REP)
            mode |= OutRep;
        else if (outputType == u"replica")
            mode |= OutReplica;
        else if (outputType == u"source")
            mode |= OutSource;
        else if (outputType == u"merged")
            mode |= OutMerged;
        else {
            fprintf(stderr, PROGRAM_NAME ": Unknown output type\"%s\".\n", qPrintable(outputType));
            parser.showHelp(1);
        }
    }

    switch (files.size()) {
    case 2:
        outputFile = files.last();
        if (!(mode & (OutRep | OutSource | OutReplica))) {
            // try to figure out the Out mode from file extension
            if (outputFile.endsWith(QLatin1String(".rep")))
                mode |= OutRep;
        }
        Q_FALLTHROUGH();
    case 1:
        inputFile = files.first();
        if (!(mode & (InRep | InJson))) {
            // try to figure out the In mode from file extension
            if (inputFile.endsWith(QLatin1String(".rep")))
                mode |= InRep;
            else
                mode |= InJson;
        }
        break;
    }
    // check mode sanity
    if (!(mode & (InRep | InJson))) {
        fprintf(stderr, PROGRAM_NAME ": Unknown input type, please use -i option to specify one.\n");
        parser.showHelp(1);
    }
    if (!(mode & (OutRep | OutSource | OutReplica))) {
        fprintf(stderr, PROGRAM_NAME ": Unknown output type, please use -o option to specify one.\n");
        parser.showHelp(1);
    }
    if (mode & InRep && mode & OutRep) {
        fprintf(stderr, PROGRAM_NAME ": Invalid input/output type combination, both are rep files.\n");
        parser.showHelp(1);
    }
    if (mode & InJson && mode & OutSource) {
        fprintf(stderr, PROGRAM_NAME ": Invalid input/output type combination, both are source header files.\n");
        parser.showHelp(1);
    }

    QFile input;
    if (inputFile.isEmpty()) {
        inputFile = QStringLiteral("<stdin>");
        input.open(stdin, QIODevice::ReadOnly);
    } else {
        input.setFileName(inputFile);
        if (!input.open(QIODevice::ReadOnly)) {
            fprintf(stderr, PROGRAM_NAME ": %s: No such file.\n", qPrintable(inputFile));
            return 1;
        }
    }

    QFile output;
    if (outputFile.isEmpty()) {
        output.open(stdout, QIODevice::WriteOnly);
    } else {
        output.setFileName(outputFile);
        if (!output.open(QIODevice::WriteOnly)) {
            fprintf(stderr, PROGRAM_NAME ": could not open output file '%s': %s.\n",
                    qPrintable(outputFile), qPrintable(output.errorString()));
            return 1;
        }
    }

    if (mode & InJson) {
        QJsonDocument doc(QJsonDocument::fromJson(input.readAll()));
        input.close();
        if (!doc.isObject()) {
            fprintf(stderr, PROGRAM_NAME ": Unable to read json input.\n");
            return 0;
        }

        QJsonObject json = doc.object();

        if (!json.contains(QLatin1String("classes")) || !json[QLatin1String("classes")].isArray()) {
            fprintf(stderr, PROGRAM_NAME ": No QObject classes found.\n");
            return 0;
        }

        QJsonArray classes = json[QLatin1String("classes")].toArray();

        if (mode & OutRep) {
            CppCodeGenerator generator(&output);
            generator.generate(classes, parser.isSet(alwaysClassOption));
        } else {
            Q_ASSERT(mode & OutReplica);
            RepCodeGenerator generator(&output, classList2AST(classes));
            generator.generate(RepCodeGenerator::REPLICA, outputFile);
        }
    } else {
        Q_ASSERT(!(mode & OutRep));
        RepParser repparser(input);
        if (parser.isSet(debugOption))
            repparser.setDebug();
        if (!repparser.parse()) {
            fprintf(stderr, PROGRAM_NAME ": %s:%d: error: %s\n", qPrintable(inputFile), repparser.lineNumber(), qPrintable(repparser.errorString()));
            // if everything is okay and only the input was malformed => remove the output file
            // let's not create an empty file -- make sure the build system tries to run repc again
            // this is the same behavior other code generators exhibit (e.g. flex)
            output.remove();
            return 1;
        }

        input.close();

        RepCodeGenerator generator(&output, repparser.ast());
        if ((mode & OutMerged) == OutMerged)
            generator.generate(RepCodeGenerator::MERGED, outputFile);
        else if (mode & OutReplica)
            generator.generate(RepCodeGenerator::REPLICA, outputFile);
        else if (mode & OutSource)
            generator.generate(RepCodeGenerator::SOURCE, outputFile);
        else {
            fprintf(stderr, PROGRAM_NAME ": Unknown mode.\n");
            return 1;
        }
    }

    output.close();
    return 0;
}
