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

#include "qmltccodewriter.h"

#include <QtCore/qfileinfo.h>
#include <QtCore/qstringbuilder.h>
#include <QtCore/qstring.h>
#include <QtCore/qmap.h>
#include <QtCore/qlist.h>

#include <utility>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

static QString urlToMacro(const QString &url)
{
    QFileInfo fi(url);
    return u"Q_QMLTC_" + fi.baseName().toUpper();
}

static QString getFunctionCategory(const QmltcMethodBase &method)
{
    QString category;
    switch (method.access) {
    case QQmlJSMetaMethod::Private:
        category = u"private"_s;
        break;
    case QQmlJSMetaMethod::Protected:
        category = u"protected"_s;
        break;
    case QQmlJSMetaMethod::Public:
        category = u"public"_s;
        break;
    }
    return category;
}

static QString getFunctionCategory(const QmltcMethod &method)
{
    QString category = getFunctionCategory(static_cast<const QmltcMethodBase &>(method));
    switch (method.type) {
    case QQmlJSMetaMethodType::Signal:
        category = u"Q_SIGNALS"_s;
        break;
    case QQmlJSMetaMethodType::Slot:
        category += u" Q_SLOTS"_s;
        break;
    case QQmlJSMetaMethodType::Method:
    case QQmlJSMetaMethodType::StaticMethod:
        break;
    }
    return category;
}

static QString appendSpace(const QString &s)
{
    if (s.isEmpty())
        return s;
    return s + u" ";
}

static QString prependSpace(const QString &s)
{
    if (s.isEmpty())
        return s;
    return u" " + s;
}

static std::pair<QString, QString> functionSignatures(const QmltcMethodBase &method)
{
    const QString name = method.name;
    const QList<QmltcVariable> &parameterList = method.parameterList;

    QStringList headerParamList;
    QStringList cppParamList;
    for (const QmltcVariable &variable : parameterList) {
        const QString commonPart = variable.cppType + u" " + variable.name;
        cppParamList << commonPart;
        headerParamList << commonPart;
        if (!variable.defaultValue.isEmpty())
            headerParamList.back() += u" = " + variable.defaultValue;
    }

    const QString headerSignature = name + u"(" + headerParamList.join(u", "_s) + u")"
            + prependSpace(method.modifiers.join(u" "));
    const QString cppSignature = name + u"(" + cppParamList.join(u", "_s) + u")"
            + prependSpace(method.modifiers.join(u" "));
    return { headerSignature, cppSignature };
}

static QString functionReturnType(const QmltcMethod &m)
{
    return appendSpace(m.declarationPrefixes.join(u" "_s)) + m.returnType;
}

void QmltcCodeWriter::writeGlobalHeader(QmltcOutputWrapper &code, const QString &sourcePath,
                                        const QString &hPath, const QString &cppPath,
                                        const QString &outNamespace,
                                        const QSet<QString> &requiredCppIncludes)
{
    Q_UNUSED(cppPath);
    const QString preamble = u"// This code is auto-generated by the qmltc tool from the file '"
            + sourcePath + u"'\n// WARNING! All changes made in this file will be lost!\n";
    code.rawAppendToHeader(preamble);
    code.rawAppendToCpp(preamble);
    code.rawAppendToHeader(
            u"// NOTE: This generated API is to be considered implementation detail.");
    code.rawAppendToHeader(
            u"//       It may change from version to version and should not be relied upon.");

    const QString headerMacro = urlToMacro(sourcePath);
    code.rawAppendToHeader(u"#ifndef %1_H"_s.arg(headerMacro));
    code.rawAppendToHeader(u"#define %1_H"_s.arg(headerMacro));

    code.rawAppendToHeader(u"#include <QtCore/qproperty.h>");
    code.rawAppendToHeader(u"#include <QtCore/qobject.h>");
    code.rawAppendToHeader(u"#include <QtCore/qcoreapplication.h>");
    code.rawAppendToHeader(u"#include <QtCore/qxpfunctional.h>");
    code.rawAppendToHeader(u"#include <QtQml/qqmlengine.h>");
    code.rawAppendToHeader(u"#include <QtCore/qurl.h>"); // used in engine execution
    code.rawAppendToHeader(u"#include <QtQml/qqml.h>"); // used for attached properties

    code.rawAppendToHeader(u"#include <private/qqmlengine_p.h>"); // executeRuntimeFunction(), etc.
    code.rawAppendToHeader(u"#include <private/qqmltcobjectcreationhelper_p.h>"); // QmltcSupportLib

    code.rawAppendToHeader(u"#include <QtQml/qqmllist.h>"); // QQmlListProperty

    // include custom C++ includes required by used types
    code.rawAppendToHeader(u"// BEGIN(custom_cpp_includes)");
    for (const auto &requiredInclude : requiredCppIncludes)
        code.rawAppendToHeader(u"#include \"" + requiredInclude + u"\"");
    code.rawAppendToHeader(u"// END(custom_cpp_includes)");

    code.rawAppendToCpp(u"#include \"" + hPath + u"\""); // include own .h file
    code.rawAppendToCpp(u"// qmltc support library:");
    code.rawAppendToCpp(u"#include <private/qqmlcppbinding_p.h>"); // QmltcSupportLib
    code.rawAppendToCpp(u"#include <private/qqmlcpponassignment_p.h>"); // QmltcSupportLib
    code.rawAppendToHeader(u"#include <private/qqmlcpptypehelpers_p.h> "); // QmltcSupportLib

    code.rawAppendToCpp(u"#include <private/qqmlobjectcreator_p.h>"); // createComponent()
    code.rawAppendToCpp(u"#include <private/qqmlcomponent_p.h>"); // QQmlComponentPrivate::get()

    code.rawAppendToCpp(u"");
    code.rawAppendToCpp(u"#include <private/qobject_p.h>"); // NB: for private properties
    code.rawAppendToCpp(u"#include <private/qqmlobjectcreator_p.h>"); // for finalize callbacks
    code.rawAppendToCpp(u"#include <QtQml/qqmlprivate.h>"); // QQmlPrivate::qmlExtendedObject()

    code.rawAppendToCpp(u""); // blank line
    code.rawAppendToCpp(u"QT_USE_NAMESPACE // avoid issues with QT_NAMESPACE");

    code.rawAppendToHeader(u""); // blank line

    const QStringList namespaces = outNamespace.split(u"::"_s);

    for (const QString &currentNamespace : namespaces) {
        code.rawAppendToHeader(u"namespace %1 {"_s.arg(currentNamespace));
        code.rawAppendToCpp(u"namespace %1 {"_s.arg(currentNamespace));
    }
}

void QmltcCodeWriter::write(QmltcOutputWrapper &code,
                            const QmltcPropertyInitializer &propertyInitializer,
                            const QmltcType &wrappedType)
{
    code.rawAppendToHeader(u"class " + propertyInitializer.name + u" {");

    {
        {
            [[maybe_unused]] QmltcOutputWrapper::HeaderIndentationScope headerIndent(&code);

            code.rawAppendToHeader(u"friend class " + wrappedType.cppType + u";");
        }

        code.rawAppendToHeader(u"public:"_s);

        [[maybe_unused]] QmltcOutputWrapper::MemberNameScope typeScope(&code, propertyInitializer.name);
        {
            [[maybe_unused]] QmltcOutputWrapper::HeaderIndentationScope headerIndent(&code);

            write(code, propertyInitializer.constructor);
            code.rawAppendToHeader(u""); // blank line

            for (const auto &propertySetter : propertyInitializer.propertySetters) {
                write(code, propertySetter);
            }
        }

        code.rawAppendToHeader(u""); // blank line
        code.rawAppendToHeader(u"private:"_s);

        {
            [[maybe_unused]] QmltcOutputWrapper::HeaderIndentationScope headerIndent(&code);

            write(code, propertyInitializer.component);
            write(code, propertyInitializer.initializedCache);
        }
    }

    code.rawAppendToHeader(u"};"_s);
    code.rawAppendToHeader(u""); // blank line
}

void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcRequiredPropertiesBundle &requiredPropertiesBundle)
{
    code.rawAppendToHeader(u"struct " + requiredPropertiesBundle.name + u" {");

    {
        [[maybe_unused]] QmltcOutputWrapper::HeaderIndentationScope headerIndent(&code);

        for (const auto &member : requiredPropertiesBundle.members) {
            write(code, member);
        }
    }

    code.rawAppendToHeader(u"};"_s);
    code.rawAppendToHeader(u""); // blank line
}

void QmltcCodeWriter::writeGlobalFooter(QmltcOutputWrapper &code, const QString &sourcePath,
                                        const QString &outNamespace)
{
    const QStringList namespaces = outNamespace.split(u"::"_s);

    for (auto it = namespaces.crbegin(), end = namespaces.crend(); it != end; it++) {
        code.rawAppendToCpp(u"} // namespace %1"_s.arg(*it));
        code.rawAppendToHeader(u"} // namespace %1"_s.arg(*it));
    }

    code.rawAppendToHeader(u""); // blank line
    code.rawAppendToHeader(u"#endif // %1_H"_s.arg(urlToMacro(sourcePath)));
    code.rawAppendToHeader(u""); // blank line
}

static void writeToFile(const QString &path, const QByteArray &data)
{
    // When not using dependency files, changing a single qml invalidates all
    // qml files and would force the recompilation of everything. To avoid that,
    // we check if the data is equal to the existing file, if yes, don't touch
    // it so the build system will not recompile unnecessary things.
    //
    // If the build system use dependency file, we should anyway touch the file
    // so qmltc is not re-run
    QFileInfo fi(path);
    if (fi.exists() && fi.size() == data.size()) {
        QFile oldFile(path);
        if (oldFile.open(QIODevice::ReadOnly)) {
            if (oldFile.readAll() == data)
                return;
        }
    }
    QFile file(path);
    if (!file.open(QIODevice::WriteOnly))
        qFatal("Could not open file %s", qPrintable(path));
    file.write(data);
}

void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcProgram &program)
{
    writeGlobalHeader(code, program.url, program.hPath, program.cppPath, program.outNamespace,
                      program.includes);

    // url method comes first
    writeUrl(code, program.urlMethod);

    // forward declare all the types first
    for (const QmltcType &type : std::as_const(program.compiledTypes))
        code.rawAppendToHeader(u"class " + type.cppType + u";");
    // write all the types and their content
    for (const QmltcType &type : std::as_const(program.compiledTypes))
        write(code, type, program.exportMacro);

    // add typeCount definitions. after all types have been written down (so
    // they are now complete types as per C++). practically, this only concerns
    // document root type
    for (const QmltcType &type : std::as_const(program.compiledTypes)) {
        if (!type.typeCount)
            continue;
        code.rawAppendToHeader(u""); // blank line
        code.rawAppendToHeader(u"constexpr %1 %2::%3()"_s.arg(type.typeCount->returnType,
                                                              type.cppType, type.typeCount->name));
        code.rawAppendToHeader(u"{");
        for (const QString &line : std::as_const(type.typeCount->body))
            code.rawAppendToHeader(line, 1);
        code.rawAppendToHeader(u"}");
    }

    writeGlobalFooter(code, program.url, program.outNamespace);

    writeToFile(program.hPath, code.code().header.toUtf8());
    writeToFile(program.cppPath, code.code().cpp.toUtf8());
}

template<typename Predicate>
static void dumpFunctions(QmltcOutputWrapper &code, const QList<QmltcMethod> &functions,
                          Predicate pred)
{
    // functions are _ordered_ by access and kind. ordering is important to
    // provide consistent output
    QMap<QString, QList<const QmltcMethod *>> orderedFunctions;
    for (const auto &function : functions) {
        if (pred(function))
            orderedFunctions[getFunctionCategory(function)].append(std::addressof(function));
    }

    for (auto it = orderedFunctions.cbegin(); it != orderedFunctions.cend(); ++it) {
        code.rawAppendToHeader(it.key() + u":", -1);
        for (const QmltcMethod *function : std::as_const(it.value()))
            QmltcCodeWriter::write(code, *function);
    }
}

void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcType &type,
                            const QString &exportMacro)
{
    const auto constructClassString = [&]() {
        QString str = u"class "_s;
        if (!exportMacro.isEmpty())
            str.append(exportMacro).append(u" "_s);
        str.append(type.cppType);
        QStringList nonEmptyBaseClasses;
        nonEmptyBaseClasses.reserve(type.baseClasses.size());
        std::copy_if(type.baseClasses.cbegin(), type.baseClasses.cend(),
                     std::back_inserter(nonEmptyBaseClasses),
                     [](const QString &entry) { return !entry.isEmpty(); });
        if (!nonEmptyBaseClasses.isEmpty())
            str += u" : public " + nonEmptyBaseClasses.join(u", public "_s);
        return str;
    };

    code.rawAppendToHeader(u""); // blank line
    code.rawAppendToCpp(u""); // blank line

    code.rawAppendToHeader(constructClassString());
    code.rawAppendToHeader(u"{");
    for (const QString &mocLine : std::as_const(type.mocCode))
        code.rawAppendToHeader(mocLine, 1);

    QmltcOutputWrapper::MemberNameScope typeScope(&code, type.cppType);
    Q_UNUSED(typeScope);
    {
        QmltcOutputWrapper::HeaderIndentationScope headerIndent(&code);
        Q_UNUSED(headerIndent);

        // first, write user-visible code, then everything else. someone might
        // want to look at the generated code, so let's make an effort when
        // writing it down

        code.rawAppendToHeader(u"/* ----------------- */");
        code.rawAppendToHeader(u"/* External C++ API */");
        code.rawAppendToHeader(u"public:", -1);

        if (!type.propertyInitializer.name.isEmpty())
            write(code, type.propertyInitializer, type);

        if (type.requiredPropertiesBundle)
            write(code, *type.requiredPropertiesBundle);

        // NB: when non-document root, the externalCtor won't be public - but we
        // really don't care about the output format of such types
        if (!type.ignoreInit && type.externalCtor.access == QQmlJSMetaMethod::Public) {
            // TODO: ignoreInit must be eliminated

            QmltcCodeWriter::write(code, type.externalCtor);
            if (type.staticCreate)
                QmltcCodeWriter::write(code, *type.staticCreate);
        }

        // dtor
        if (type.dtor)
            QmltcCodeWriter::write(code, *type.dtor);

        // enums
        for (const auto &enumeration : std::as_const(type.enums))
            QmltcCodeWriter::write(code, enumeration);

        // visible functions
        const auto isUserVisibleFunction = [](const QmltcMethod &function) {
            return function.userVisible;
        };
        dumpFunctions(code, type.functions, isUserVisibleFunction);

        code.rawAppendToHeader(u"/* ----------------- */");
        code.rawAppendToHeader(u""); // blank line
        code.rawAppendToHeader(u"/* Internal functionality (do NOT use it!) */");

        // below are the hidden parts of the type

        // (rest of the) ctors
        if (type.ignoreInit) { // TODO: this branch should be eliminated
            Q_ASSERT(type.baselineCtor.access == QQmlJSMetaMethod::Public);
            code.rawAppendToHeader(u"public:", -1);
            QmltcCodeWriter::write(code, type.baselineCtor);
        } else {
            code.rawAppendToHeader(u"protected:", -1);
            if (type.externalCtor.access != QQmlJSMetaMethod::Public) {
                Q_ASSERT(type.externalCtor.access == QQmlJSMetaMethod::Protected);
                QmltcCodeWriter::write(code, type.externalCtor);
            }
            QmltcCodeWriter::write(code, type.baselineCtor);
            QmltcCodeWriter::write(code, type.init);
            QmltcCodeWriter::write(code, type.endInit);
            QmltcCodeWriter::write(code, type.setComplexBindings);
            QmltcCodeWriter::write(code, type.beginClass);
            QmltcCodeWriter::write(code, type.completeComponent);
            QmltcCodeWriter::write(code, type.finalizeComponent);
            QmltcCodeWriter::write(code, type.handleOnCompleted);
        }

        // children
        for (const auto &child : std::as_const(type.children))
            QmltcCodeWriter::write(code, child, exportMacro);

        // (non-visible) functions
        dumpFunctions(code, type.functions, std::not_fn(isUserVisibleFunction));

        // variables and properties
        if (!type.variables.isEmpty() || !type.properties.isEmpty()) {
            code.rawAppendToHeader(u""); // blank line
            code.rawAppendToHeader(u"protected:", -1);
        }
        for (const auto &property : std::as_const(type.properties))
            write(code, property);
        for (const auto &variable : std::as_const(type.variables))
            write(code, variable);
    }

    code.rawAppendToHeader(u"private:", -1);
    for (const QString &otherLine : std::as_const(type.otherCode))
        code.rawAppendToHeader(otherLine, 1);

    if (type.typeCount) {
        // add typeCount declaration, definition is added later
        code.rawAppendToHeader(u""); // blank line
        code.rawAppendToHeader(u"protected:");
        code.rawAppendToHeader(u"constexpr static %1 %2();"_s.arg(type.typeCount->returnType,
                                                                  type.typeCount->name),
                               1);
    }

    code.rawAppendToHeader(u"};");
}

void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcEnum &enumeration)
{
    code.rawAppendToHeader(u"enum " + enumeration.cppType + u" {");
    for (qsizetype i = 0; i < enumeration.keys.size(); ++i) {
        QString str;
        if (enumeration.values.isEmpty()) {
            str += enumeration.keys.at(i) + u",";
        } else {
            str += enumeration.keys.at(i) + u" = " + enumeration.values.at(i) + u",";
        }
        code.rawAppendToHeader(str, 1);
    }
    code.rawAppendToHeader(u"};");
    code.rawAppendToHeader(enumeration.ownMocLine);
}

void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcMethod &method)
{
    const auto [hSignature, cppSignature] = functionSignatures(method);
    // Note: augment return type with preambles in declaration
    code.rawAppendToHeader((method.type == QQmlJSMetaMethodType::StaticMethod
                                    ? u"static " + functionReturnType(method)
                                    : functionReturnType(method))
                           + u" " + hSignature + u";");

    // do not generate method implementation if it is a signal
    const auto methodType = method.type;
    if (methodType != QQmlJSMetaMethodType::Signal) {
        code.rawAppendToCpp(u""_s); // blank line
        if (method.comments.size() > 0) {
            code.rawAppendToCpp(u"/*! \\internal"_s);
            for (const auto &comment : method.comments)
                code.rawAppendToCpp(comment, 1);
            code.rawAppendToCpp(u"*/"_s);
        }
        code.rawAppendToCpp(method.returnType);
        code.rawAppendSignatureToCpp(cppSignature);
        code.rawAppendToCpp(u"{");
        {
            QmltcOutputWrapper::CppIndentationScope cppIndent(&code);
            Q_UNUSED(cppIndent);
            for (const QString &line : std::as_const(method.body))
                code.rawAppendToCpp(line);
        }
        code.rawAppendToCpp(u"}");
    }
}

template<typename WriteInitialization>
static void writeSpecialMethod(QmltcOutputWrapper &code, const QmltcMethodBase &specialMethod,
                               WriteInitialization writeInit)
{
    const auto [hSignature, cppSignature] = functionSignatures(specialMethod);
    code.rawAppendToHeader(hSignature + u";");

    code.rawAppendToCpp(u""); // blank line
    code.rawAppendSignatureToCpp(cppSignature);

    writeInit(specialMethod);

    code.rawAppendToCpp(u"{");
    {
        QmltcOutputWrapper::CppIndentationScope cppIndent(&code);
        Q_UNUSED(cppIndent);
        for (const QString &line : std::as_const(specialMethod.body))
            code.rawAppendToCpp(line);
    }
    code.rawAppendToCpp(u"}");
}

void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcCtor &ctor)
{
    const auto writeInitializerList = [&](const QmltcMethodBase &ctorBase) {
        auto ctor = static_cast<const QmltcCtor &>(ctorBase);
        if (!ctor.initializerList.isEmpty()) {
            code.rawAppendToCpp(u":", 1);
            // double \n to make separate initializer list lines stand out more
            code.rawAppendToCpp(
                    ctor.initializerList.join(u",\n\n" + u"    "_s.repeated(code.cppIndent + 1)),
                    1);
        }
    };

    writeSpecialMethod(code, ctor, writeInitializerList);
}

void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcDtor &dtor)
{
    const auto noop = [](const QmltcMethodBase &) {};
    writeSpecialMethod(code, dtor, noop);
}

void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcVariable &var)
{
    const QString optionalPart = var.defaultValue.isEmpty() ? u""_s : u" = " + var.defaultValue;
    code.rawAppendToHeader(var.cppType + u" " + var.name + optionalPart + u";");
}

void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcProperty &prop)
{
    Q_ASSERT(prop.defaultValue.isEmpty()); // we don't support it yet (or at all?)
    code.rawAppendToHeader(u"Q_OBJECT_BINDABLE_PROPERTY(%1, %2, %3, &%1::%4)"_s.arg(
            prop.containingClass, prop.cppType, prop.name, prop.signalName));
}

void QmltcCodeWriter::writeUrl(QmltcOutputWrapper &code, const QmltcMethod &urlMethod)
{
    // unlike ordinary methods, url function only exists in .cpp
    Q_ASSERT(!urlMethod.returnType.isEmpty());
    const auto [hSignature, _] = functionSignatures(urlMethod);
    Q_UNUSED(_);
    // Note: augment return type with preambles in declaration
    code.rawAppendToCpp(functionReturnType(urlMethod) + u" " + hSignature);
    code.rawAppendToCpp(u"{");
    {
        QmltcOutputWrapper::CppIndentationScope cppIndent(&code);
        Q_UNUSED(cppIndent);
        for (const QString &line : std::as_const(urlMethod.body))
            code.rawAppendToCpp(line);
    }
    code.rawAppendToCpp(u"}");
}

QT_END_NAMESPACE
