// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "qgrpcgenerator.h"
#include "clientdeclarationprinter.h"
#include "clientdefinitionprinter.h"
#include "qmlclientdeclarationprinter.h"
#include "qmlclientdefinitionprinter.h"
#include "serverdeclarationprinter.h"

#include "grpctemplates.h"
#include "utils.h"
#include "options.h"

#include <google/protobuf/compiler/code_generator.h>
#include <google/protobuf/descriptor.h>
#include <google/protobuf/io/printer.h>
#include <google/protobuf/io/zero_copy_stream.h>
#include <google/protobuf/stubs/common.h>

using namespace ::QtGrpc;
using namespace ::qtprotoccommon;
using namespace ::google::protobuf;
using namespace ::google::protobuf::io;
using namespace ::google::protobuf::compiler;

namespace {

const utils::ExternalIncludesOrderedSet &externalQmlIncludes()
{
    static const utils::ExternalIncludesOrderedSet externalQmlIncludes{
        "QtQml/qqmlengine.h",
        "QtQml/qjsengine.h",
        "QtQml/qjsvalue.h",
        "QtGrpcQuick/qqmlgrpcfunctionalhandlers.h",
        "QtGrpcQuick/qtqmlgrpcstreamsender.h",
        "QtGrpcQuick/qqmlgrpccalloptions.h"
    };

    return externalQmlIncludes;
}

const utils::ExternalIncludesOrderedSet &externalIncludes()
{
    static const utils::ExternalIncludesOrderedSet externalIncludes{
        "QtGrpc/qgrpcclientbase.h",
        "QtGrpc/qgrpccallreply.h",
        "QtGrpc/qgrpcstream.h",
    };
    return externalIncludes;
}

const std::set<std::string> &systemIncludes()
{
    static const std::set<std::string> systemIncludes{
        "memory",
    };
    return systemIncludes;
}

} // namespace

QGrpcGenerator::QGrpcGenerator() : GeneratorBase()
{}

QGrpcGenerator::~QGrpcGenerator() = default;

bool QGrpcGenerator::Generate(const FileDescriptor *file,
                              [[maybe_unused]] const std::string &parameter,
                              GeneratorContext *generatorContext,
                              [[maybe_unused]] std::string *error) const
{
    assert(file != nullptr && generatorContext != nullptr);

    return GenerateClientServices(file, generatorContext);
}

std::set<std::string> QGrpcGenerator::GetInternalIncludes(const FileDescriptor *file)
{
    std::set<std::string> includes;
    std::string fullSuffix = CommonTemplates::ProtoFileSuffix();
    fullSuffix += CommonTemplates::HeaderSuffix();

    assert(file != nullptr);
    for (int i = 0; i < file->service_count(); ++i) {
        const ServiceDescriptor *service = file->service(i);
        for (int j = 0; j < service->method_count(); ++j) {
            const MethodDescriptor *method = service->method(j);
            if (method->input_type()->file() != service->file()) {
                includes.insert(utils::removeFileSuffix(method->input_type()->file()->name())
                                + fullSuffix);
            }

            if (method->output_type()->file() != service->file()) {
                includes.insert(utils::removeFileSuffix(method->output_type()->file()->name())
                                + fullSuffix);
            }
        }
    }
    if (file->message_type_count() > 0) {
        includes.insert(common::generateRelativeFilePath(file,
                                                         utils::extractFileBasename(file->name()))
                        + fullSuffix);
    }
    return includes;
}

template <typename ServicePrinterT>
void QGrpcGenerator::RunPrinter(const FileDescriptor *file, std::shared_ptr<Printer> printer) const
{
    assert(file != nullptr);
    OpenFileNamespaces(file, printer.get());
    for (int i = 0; i < file->service_count(); ++i) {
        const ServiceDescriptor *service = file->service(i);

        ServicePrinterT servicePrinter(service, printer);
        servicePrinter.run();
    }
    CloseFileNamespaces(file, printer.get());
}

void QGrpcGenerator::GenerateQmlClientServices(
        const ::google::protobuf::FileDescriptor *file,
        ::google::protobuf::compiler::GeneratorContext *generatorContext) const
{
    assert(file != nullptr);
    assert(generatorContext != nullptr);

    const std::string qmlPrefix = "qml";

    const std::string basename = utils::extractFileBasename(file->name()) +
        GrpcTemplates::GrpcClientFileSuffix() + CommonTemplates::ProtoFileSuffix();
    const std::string qmlBasename = qmlPrefix + basename;

    const std::string realtivePath = common::generateRelativeFilePath(file, basename);
    const std::string qmlRealtivePath = qmlPrefix + realtivePath;

    // QML registered client class
    std::unique_ptr<ZeroCopyOutputStream>
        clientQmlHeaderStream(generatorContext->Open(qmlRealtivePath
                                                     + CommonTemplates::HeaderSuffix()));
    std::unique_ptr<ZeroCopyOutputStream> clientQmlSourceStream(
                generatorContext->Open(qmlRealtivePath + ".cpp"));

    std::shared_ptr<Printer> qmlHeaderPrinter(new Printer(clientQmlHeaderStream.get(), '$'));
    std::shared_ptr<Printer> qmlSourcePrinter(new Printer(clientQmlSourceStream.get(), '$'));

    printDisclaimer(qmlHeaderPrinter.get());
    printDisclaimer(qmlSourcePrinter.get());

    std::string headerGuard = common::headerGuardFromFilename(qmlBasename
                                                              + CommonTemplates::HeaderSuffix());
    QGrpcGenerator::printHeaderGuardBegin(qmlHeaderPrinter.get(), headerGuard);

    printIncludes(qmlHeaderPrinter.get(), { realtivePath + CommonTemplates::HeaderSuffix() },
                  externalQmlIncludes(), {});

    qmlSourcePrinter->Print(
        {
            { "include", qmlRealtivePath + CommonTemplates::HeaderSuffix() }
    },
        CommonTemplates::InternalIncludeTemplate());

    QGrpcGenerator::RunPrinter<QmlClientDeclarationPrinter>(file, qmlHeaderPrinter);
    QGrpcGenerator::RunPrinter<QmlClientDefinitionPrinter>(file, qmlSourcePrinter);

    QGrpcGenerator::printHeaderGuardEnd(qmlHeaderPrinter.get(), headerGuard);
}

bool QGrpcGenerator::GenerateClientServices(const FileDescriptor *file,
                                            GeneratorContext *generatorContext) const
{
    assert(file != nullptr && generatorContext != nullptr);
    if (file->service_count() <= 0)
        return true;

    const std::string basename = utils::extractFileBasename(file->name()) +
        GrpcTemplates::GrpcClientFileSuffix() + CommonTemplates::ProtoFileSuffix();
    std::string identifier = utils::toValidIdentifier(basename);
    const std::string realtivePath = common::generateRelativeFilePath(file, basename);

    // Generate QML class
    if (Options::instance().hasQml())
        GenerateQmlClientServices(file, generatorContext);

    // CPP client class
    std::unique_ptr<ZeroCopyOutputStream>
        clientHeaderStream(generatorContext->Open(realtivePath + CommonTemplates::HeaderSuffix()));
    std::unique_ptr<ZeroCopyOutputStream> clientSourceStream(
                generatorContext->Open(realtivePath + ".cpp"));

    std::shared_ptr<Printer> clientHeaderPrinter(new Printer(clientHeaderStream.get(), '$'));
    std::shared_ptr<Printer> clientSourcePrinter(new Printer(clientSourceStream.get(), '$'));

    printDisclaimer(clientHeaderPrinter.get());
    printDisclaimer(clientSourcePrinter.get());

    const std::string
        headerGuard = common::headerGuardFromFilename(identifier + CommonTemplates::HeaderSuffix());
    QGrpcGenerator::printHeaderGuardBegin(clientHeaderPrinter.get(), headerGuard);

    clientSourcePrinter->Print(
        {
            { "include", realtivePath + CommonTemplates::HeaderSuffix() }
    },
        CommonTemplates::InternalIncludeTemplate());

    std::set<std::string> internalIncludes = QGrpcGenerator::GetInternalIncludes(file);
    if (auto macroFilename = Options::instance().exportMacroFilename(); !macroFilename.empty())
        internalIncludes.emplace(std::move(macroFilename));

    printIncludes(clientHeaderPrinter.get(), internalIncludes, externalIncludes(),
                  systemIncludes());

    QGrpcGenerator::RunPrinter<ClientDeclarationPrinter>(file, clientHeaderPrinter);
    QGrpcGenerator::RunPrinter<ClientDefinitionPrinter>(file, clientSourcePrinter);

    QGrpcGenerator::printHeaderGuardEnd(clientHeaderPrinter.get(), headerGuard);
    return true;
}

bool QGrpcGenerator::GenerateAll(const std::vector<const FileDescriptor *> &files,
                                 const std::string &parameter, GeneratorContext *generatorContext,
                                 std::string *error) const
{
    Options::setFromString(parameter, qtprotoccommon::Options::QtGrpcGen, error);
    if (!error->empty())
        return false;
    return GeneratorBase::GenerateAll(files, parameter, generatorContext, error);
}
