// Copyright (C) 2023 basysKom GmbH, opensource@basyskom.com
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "nodeidgenerator.h"
#include "recursivedescentparser.h"

#include <QtCore/qcommandlineoption.h>
#include <QtCore/qcommandlineparser.h>
#include <QtCore/qdebug.h>
#include <QtCore/qfile.h>
#include <QtCore/qregularexpression.h>
#include <QtCore/qstring.h>
#include <QtCore/qstringlist.h>
#include <QtCore/qtextstream.h>
#include <QtCore/qxmlstream.h>

#include <cstdlib>

using namespace Qt::Literals::StringLiterals;

bool readBsdFile(RecursiveDescentParser &recursiveDescentParser,
                 const QString &fileName,
                 bool dependencyInput)
{
    switch (recursiveDescentParser.parseFile(fileName, dependencyInput)) {
    case RecursiveDescentParser::NoError:
        return true;
    case RecursiveDescentParser::InvalidFileName:
        qCritical() << "Error: File does not exist:" << fileName;
        return false;
    case RecursiveDescentParser::InvalidTypeDictionaryEntry:
        qCritical() << "Error: Invalid TypeDictionary entry in" << fileName;
        return false;
    case RecursiveDescentParser::InvalidStructuredTypeEntry:
        qCritical() << "Error: Invalid StructuredType entry in" << fileName;
        return false;
    case RecursiveDescentParser::InvalidEnumeratedTypeEntry:
        qCritical() << "Error: Invalid EnumeratedType entry in" << fileName;
        return false;
    case RecursiveDescentParser::InvalidImportEntry:
        qCritical() << "Error: Invalid Import entry in" << fileName;
        return false;
    case RecursiveDescentParser::InvalidFieldEntry:
        qCritical() << "Error: Invalid Field entry in" << fileName;
        return false;
    case RecursiveDescentParser::InvalidEnumeratedValueEntry:
        qCritical() << "Error: Invalid EnumeratedValue entry in" << fileName;
        return false;
    case RecursiveDescentParser::CannotFullyGenerateNamespaceZero:
        qCritical() << "Error: Full generation of namespace 0 is currently not "
                       "supported";
        return false;
    case RecursiveDescentParser::MissingDependency:
        qCritical() << "Error: Missing dependency Type found in" << fileName;
        return false;
    default:
        qCritical() << "Error: Unknown parsing error occurred";
        return false;
    }
}

bool generateBsdFiles(RecursiveDescentParser &recursiveDescentParser,
                      const QString &outputPath,
                      const QString &dataPrefix,
                      const QString &outputFileHeader,
                      bool generateBundleFiles)
{
    switch (recursiveDescentParser.generateInputFiles(outputPath,
                                                      dataPrefix,
                                                      outputFileHeader,
                                                      generateBundleFiles)) {
    case RecursiveDescentParser::NoError:
        return true;
    case RecursiveDescentParser::UnableToWriteFile:
        qCritical() << "Error: Unable to write files at specified path.";
        return false;
    case RecursiveDescentParser::MissingDependency:
        qCritical() << "Error: Unresolved dependent type occurred.";
        return false;
    case RecursiveDescentParser::UnableToResolveDependency:
        qCritical() << "Error: Unresolvable mapping occurred.";
        return false;
    default:
        qCritical() << "Error: Unknown file generating error occurred.";
        return false;
    }
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    const QString appName = u"qopcuaxmldatatypes2cpp"_s;
    const QString appVersion = u"1.1"_s;

    auto arguments = a.arguments();
    arguments.replace(0, appName);

    const auto outputFileHeader = u"/*\n"
                                  " * This file was generated by %1 version %2\n"
                                  " * Command line used: %3\n"
                                  " */"_s
                                      .arg(appName,
                                           appVersion,
                                           arguments.join(QLatin1Char(' ')));

    QCoreApplication::setApplicationName(appName);
    QCoreApplication::setApplicationVersion(appVersion);
    QCommandLineParser parser;
    parser.setApplicationDescription(
        u"Code generator for custom data models.\n"
        "Converts OPC UA .bsd files into enums and C++ data classes and generates a class "
        "to decode and encode the values from/to a QOpcUaExtensionObject with binary body or a QByteArray."_s);
    parser.addHelpOption();
    parser.addVersionOption();

    const QCommandLineOption inputFileOption(QStringList() << u"i"_s
                                                     << u"input"_s,
                                       u"A primary input file. Will generate code for all contained types and "
                                       "check for missing dependencies"_s,
                                       u"file"_s);
    parser.addOption(inputFileOption);
    const QCommandLineOption nodeIdFileOption(QStringList() << u"n"_s
                                                           << u"nodeids"_s,
                                             u"A CSV file with NodeIds. Will add an enum class <name>NodeId to the <prefix>nodeids.h file"_s,
                                             u"name:path"_s);
    parser.addOption(nodeIdFileOption);
    const QCommandLineOption inputDependencyFileOption(
        QStringList() << u"d"_s
                      << u"dependencyinput"_s,
        u"A dependency input file. Only types required by primary input files will be generated"_s,
        u"file"_s);
    parser.addOption(inputDependencyFileOption);
    const QCommandLineOption outputDirectoryPathOption(QStringList() << u"o"_s
                                                                     << u"output"_s,
                                                       u"output directory for the generated C++ files."_s,
                                                       u"path"_s);
    parser.addOption(outputDirectoryPathOption);
    const QCommandLineOption
        outputPrefixOption(QStringList() << u"p"_s
                                         << u"prefix"_s,
                           u"prefix for the generated files, default is GeneratedOpcUa"_s,
                           u"prefix"_s,
                           u"GeneratedOpcUa"_s);
    parser.addOption(outputPrefixOption);
    const QCommandLineOption bundleFilesOption(QStringList() << u"b"_s << u"bundle"_s,
                                               u"Create bundle .h and .cpp file"_s);
    parser.addOption(bundleFilesOption);

    parser.process(a);

    if (!parser.isSet(inputFileOption) && !parser.isSet(nodeIdFileOption)) {
        qCritical() << "Error: At least one bsd or csv input file must be specified";
        parser.showHelp(1);
        return EXIT_FAILURE;
    }

    if (!parser.isSet(outputDirectoryPathOption)) {
        qCritical() << "Error: The output path must be specified.";
        parser.showHelp(1);
        return EXIT_FAILURE;
    }

    if (parser.values(outputPrefixOption).size() > 1)
        qInfo() << "Info: The first output prefix will be used";
    if (!parser.values(outputPrefixOption)
             .at(0)
             .contains(QRegularExpression(u"^[A-Za-z]+[A-Za-z0-9]*$"_s))) {
        qCritical() << "Error: The prefix contains illegal characters";
        qInfo() << "Info: The prefix must consist of letters and numbers and start with a letter";
        return EXIT_FAILURE;
    }

    const auto dataPrefix = parser.value(outputPrefixOption);

    NodeIdGenerator nodeIdGen;

    if (parser.isSet(nodeIdFileOption)) {
        const auto nodeIdEntries = parser.values(nodeIdFileOption);

        for (const auto &entry : nodeIdEntries) {
            const auto index = entry.indexOf(QChar::fromLatin1(':'));
            if (index == -1 || index == 0 || entry.size() <= index + 1) {
                qWarning() << "Invalid value:" << entry << "- NodeId entries must be given as name:filepath";
                return EXIT_FAILURE;
            }

            const auto success = nodeIdGen.parseNodeIds(entry.first(index), entry.mid(index + 1));
            if (!success)
                return EXIT_FAILURE;
        }
    }

    if (!parser.isSet(inputFileOption) && !nodeIdGen.hasNodeIds())
        return EXIT_FAILURE;

    if (nodeIdGen.hasNodeIds()) {
        const auto success = nodeIdGen.generateNodeIdsHeader(dataPrefix, parser.value(outputDirectoryPathOption), outputFileHeader);
        if (success)
            qInfo() << "All node ids were successfully generated";

        if (!parser.isSet(inputFileOption))
            return success ? EXIT_SUCCESS : EXIT_FAILURE;
    }

    auto success = true;
    RecursiveDescentParser recursiveDescentParser;
    const QStringList inputFileNames = parser.values(inputFileOption);
    for (const auto &fileName : inputFileNames)
        success &= readBsdFile(recursiveDescentParser, fileName, false);
    const QStringList dependencyInputFileNames = parser.values(inputDependencyFileOption);
    for (const auto &fileName : dependencyInputFileNames)
        success &= readBsdFile(recursiveDescentParser, fileName, true);
    if (success) {
        const auto outputPath = parser.value(outputDirectoryPathOption);
        if (generateBsdFiles(recursiveDescentParser,
                             outputPath,
                             dataPrefix,
                             outputFileHeader,
                             parser.isSet(bundleFilesOption))) {

            qInfo() << "Info: All types were successfully generated";
            return EXIT_SUCCESS;
        }
    }
    return EXIT_FAILURE;
}
