/****************************************************************************
**
** Copyright (C) 2021 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the tools applications of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "codegeneratorwriter.h"

#include <private/qqmljsmetatypes_p.h>

#include <QtCore/qfileinfo.h>

#include <utility>
#include <functional>

static constexpr char16_t newLine[] =
#ifdef Q_OS_WIN32
        u"\r\n";
#else
        u"\n";
#endif
static constexpr char newLineLatin1[] =
#ifdef Q_OS_WIN32
        "\r\n";
#else
        "\n";
#endif

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

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

static QString getFunctionCategory(const QQmlJSAotMethod &compiled)
{
    QString category = getFunctionCategory(static_cast<const QQmlJSAotMethodBase &>(compiled));
    switch (compiled.type) {
    case QQmlJSMetaMethod::Signal:
        category = u"signals"_qs;
        break;
    case QQmlJSMetaMethod::Slot:
        category += u" slots"_qs;
        break;
    case QQmlJSMetaMethod::Method:
        break;
    }
    return category;
}

void CodeGeneratorWriter::writeGlobalHeader(GeneratedCodeUtils &code, const QString &sourceName,
                                            const QString &hPath, const QString &cppPath,
                                            const QString &outNamespace,
                                            const QSet<QString> &requiredCppIncludes)
{
    Q_UNUSED(newLineLatin1);

    Q_UNUSED(cppPath);
    const QString preamble =
            u"// This code is auto-generated by the qmlcompiler tool from the file '" + sourceName
            + u"'" + newLine + u"// WARNING! All changes made in this file will be lost!" + newLine;
    code.appendToHeader(preamble);
    code.appendToImpl(preamble);
    code.appendToHeader(u"// NOTE: This generated API is to be considered implementation detail.");
    code.appendToHeader(
            u"//       It may change from version to version and should not be relied upon.");

    const QString headerMacro = urlToMacro(sourceName);
    code.appendToHeader(u"#ifndef %1_H"_qs.arg(headerMacro));
    code.appendToHeader(u"#define %1_H"_qs.arg(headerMacro));

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

    code.appendToHeader(u"#include <private/qqmlengine_p.h>"); // NB: private header

    code.appendToHeader(u"#include <QQmlListProperty>"); // required by list properties

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

    code.appendToImpl(u"#include \"" + hPath + u"\""); // include own .h file
    code.appendToImpl(u"#include <private/qqmlcppbinding_p.h>"); // QmltcSupportLib
    code.appendToImpl(u"#include <private/qqmlcpponassignment_p.h>"); // QmltcSupportLib

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

    code.appendToImpl(u"");
    code.appendToImpl(u"#include <private/qobject_p.h>"); // NB: for private properties
    code.appendToImpl(u"#include <private/qqmlobjectcreator_p.h>"); // for finalize callbacks

    code.appendToImpl(u""); // blank line
    if (!outNamespace.isEmpty()) {
        code.appendToHeader(u""); // blank line
        code.appendToHeader(u"namespace %1 {"_qs.arg(outNamespace));
        code.appendToImpl(u""); // blank line
        code.appendToImpl(u"namespace %1 {"_qs.arg(outNamespace));
    }
}

void CodeGeneratorWriter::writeGlobalFooter(GeneratedCodeUtils &code, const QString &sourceName,
                                            const QString &hPath, const QString &cppPath,
                                            const QString &outNamespace)
{
    Q_UNUSED(code);
    Q_UNUSED(hPath);
    Q_UNUSED(cppPath);

    if (!outNamespace.isEmpty()) {
        code.appendToImpl(u"} // namespace %1"_qs.arg(outNamespace));
        code.appendToImpl(u""); // blank line
        code.appendToHeader(u"} // namespace %1"_qs.arg(outNamespace));
        code.appendToHeader(u""); // blank line
    }

    code.appendToHeader(u"#endif // %1_H"_qs.arg(urlToMacro(sourceName)));
    code.appendToHeader(u""); // blank line
}

static QString classString(const QQmlJSAotObject &compiled)
{
    QString str = u"class " + compiled.cppType;
    QStringList nonEmptyBaseClasses;
    nonEmptyBaseClasses.reserve(compiled.baseClasses.size());
    std::copy_if(compiled.baseClasses.cbegin(), compiled.baseClasses.cend(),
                 std::back_inserter(nonEmptyBaseClasses),
                 [](const QString &entry) { return !entry.isEmpty(); });
    if (!nonEmptyBaseClasses.isEmpty())
        str += u" : public " + nonEmptyBaseClasses.join(u", public "_qs);
    return str;
}

template<typename Predicate>
static void dumpFunctions(GeneratedCodeUtils &code, const QList<QQmlJSAotMethod> &functions,
                          Predicate pred)
{
    // functions are _ordered_ by access and kind. ordering is important to
    // provide consistent output
    QMap<QString, QList<const QQmlJSAotMethod *>> 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.appendToHeader(it.key() + u":", -1);
        for (const QQmlJSAotMethod *function : qAsConst(it.value()))
            CodeGeneratorWriter::write(code, *function);
    }
}

void CodeGeneratorWriter::write(GeneratedCodeUtils &code, const QQmlJSAotObject &compiled)
{
    code.appendToHeader(u""); // just new line
    code.appendToImpl(u""); // just new line

    // generate class preamble
    code.appendToHeader(classString(compiled));
    code.appendToHeader(u"{");
    for (const QString &mocLine : qAsConst(compiled.mocCode))
        code.appendToHeader(mocLine, 1);

    for (const QString &otherLine : qAsConst(compiled.otherCode))
        code.appendToHeader(otherLine, 1);

    GeneratedCodeUtils::MemberNamespaceScope thisObjectScope(code, compiled.cppType);
    Q_UNUSED(thisObjectScope);
    {
        GeneratedCodeUtils::HeaderIndentationScope headerIndentScope(code);
        Q_UNUSED(headerIndentScope);

        // 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.appendToHeader(u"// -----------------");
        code.appendToHeader(u"// External C++ API:");
        code.appendToHeader(u"public:", -1);

        // 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 (!compiled.ignoreInit && compiled.externalCtor.access == QQmlJSMetaMethod::Public) {
            // TODO: ignoreInit must be eliminated

            CodeGeneratorWriter::write(code, compiled.externalCtor);
        }
        // generate dtor
        if (compiled.dtor)
            CodeGeneratorWriter::write(code, *compiled.dtor);

        // generate enums
        for (const auto &enumeration : qAsConst(compiled.enums))
            CodeGeneratorWriter::write(code, enumeration);

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

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

        // below are the hidden parts of the class

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

            // code.appendToHeader(u"public:", -1);
        }

        // generate child types
        code.appendToHeader(u"// BEGIN(children)");
        for (const auto &child : qAsConst(compiled.children))
            CodeGeneratorWriter::write(code, child);
        code.appendToHeader(u"// END(children)");

        // generate functions
        code.appendToHeader(u"// BEGIN(hidden_functions)");
        dumpFunctions(code, compiled.functions, std::not_fn(isUserVisibleFunction));
        code.appendToHeader(u"// END(hidden_functions)");

        if (!compiled.variables.isEmpty() || !compiled.properties.isEmpty()) {
            code.appendToHeader(u""); // blank line
            code.appendToHeader(u"protected:", -1);
        }
        // generate variables
        if (!compiled.variables.isEmpty()) {
            code.appendToHeader(u"// BEGIN(variables)");
            for (const auto &variable : qAsConst(compiled.variables))
                CodeGeneratorWriter::write(code, variable);
            code.appendToHeader(u"// END(variables)");
        }

        // generate properties
        if (!compiled.properties.isEmpty()) {
            code.appendToHeader(u"// BEGIN(properties)");
            for (const auto &property : qAsConst(compiled.properties))
                CodeGeneratorWriter::write(code, property);
            code.appendToHeader(u"// END(properties)");
        }
    }

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

void CodeGeneratorWriter::write(GeneratedCodeUtils &code, const QQmlJSAotEnum &compiled)
{
    code.appendToHeader(u"enum " + compiled.cppType + u" {");
    for (qsizetype i = 0; i < compiled.keys.size(); ++i) {
        QString str;
        if (compiled.values.isEmpty()) {
            str += compiled.keys.at(i) + u",";
        } else {
            str += compiled.keys.at(i) + u" = " + compiled.values.at(i) + u",";
        }
        code.appendToHeader(str, 1);
    }
    code.appendToHeader(u"};");
    code.appendToHeader(compiled.ownMocLine);
}

// NB: property generation is only concerned with property declaration in the header
void CodeGeneratorWriter::write(GeneratedCodeUtils &code, const QQmlJSAotVariable &compiled)
{
    if (compiled.defaultValue.isEmpty()) {
        code.appendToHeader(compiled.cppType + u" " + compiled.name + u";");
    } else {
        code.appendToHeader(compiled.cppType + u" " + compiled.name + u" = " + compiled.defaultValue
                            + u";");
    }
}

void CodeGeneratorWriter::write(GeneratedCodeUtils &code, const QQmlJSAotProperty &prop)
{
    Q_ASSERT(prop.defaultValue.isEmpty()); // we don't support it yet
    code.appendToHeader(u"Q_OBJECT_BINDABLE_PROPERTY(%1, %2, %3, &%1::%4)"_qs.arg(
            prop.containingClass, prop.cppType, prop.name, prop.signalName));
}

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 QQmlJSAotMethodBase &m)
{
    const QString name = m.name;
    const QList<QQmlJSAotVariable> &parameterList = m.parameterList;
    QStringList headerParamList;
    QStringList implParamList;
    for (const QQmlJSAotVariable &variable : parameterList) {
        const QString commonPart = variable.cppType + u" " + variable.name;
        implParamList << commonPart;
        headerParamList << commonPart;
        if (!variable.defaultValue.isEmpty())
            headerParamList.back() += u" = " + variable.defaultValue;
    }
    const QString headerSignature = name + u"(" + headerParamList.join(u", "_qs) + u")"
            + prependSpace(m.modifiers.join(u" "));
    const QString implSignature = name + u"(" + implParamList.join(u", "_qs) + u")"
            + prependSpace(m.modifiers.join(u" "));
    return { headerSignature, implSignature };
}

static QString functionReturnType(const QQmlJSAotMethodBase &m)
{
    return appendSpace(m.declPreambles.join(u" "_qs)) + m.returnType;
}

void CodeGeneratorWriter::write(GeneratedCodeUtils &code, const QQmlJSAotMethod &compiled)
{
    const auto [hSignature, cppSignature] = functionSignatures(compiled);
    // Note: augment return type with preambles in declaration
    code.appendToHeader(functionReturnType(compiled) + u" " + hSignature + u";");

    // do not generate method implementation if it is a signal
    const auto methodType = compiled.type;
    if (methodType != QQmlJSMetaMethod::Signal) {
        code.appendToImpl(u""); // just new line
        code.appendToImpl(compiled.returnType);
        code.appendSignatureToImpl(cppSignature);
        code.appendToImpl(u"{");
        {
            GeneratedCodeUtils::ImplIndentationScope indentScope(code);
            Q_UNUSED(indentScope);
            for (const QString &line : qAsConst(compiled.firstLines))
                code.appendToImpl(line);
            for (const QString &line : qAsConst(compiled.body))
                code.appendToImpl(line);
            for (const QString &line : qAsConst(compiled.lastLines))
                code.appendToImpl(line);
        }
        code.appendToImpl(u"}");
    }
}

void CodeGeneratorWriter::write(GeneratedCodeUtils &code, const QQmlJSAotSpecialMethod &compiled)
{
    const auto [hSignature, cppSignature] = functionSignatures(compiled);
    const QString returnTypeWithSpace =
            compiled.returnType.isEmpty() ? u""_qs : compiled.returnType + u" ";

    code.appendToHeader(returnTypeWithSpace + hSignature + u";");

    code.appendToImpl(u""); // just new line
    if (!returnTypeWithSpace.isEmpty())
        code.appendToImpl(returnTypeWithSpace);
    code.appendSignatureToImpl(cppSignature);
    if (!compiled.initializerList.isEmpty()) {
        code.appendToImpl(u":", 1);
        code.appendToImpl(compiled.initializerList.join(u","_qs + newLine + newLine
                                                        + u"    "_qs.repeated(code.implIndent + 1)),
                          1);
    }
    code.appendToImpl(u"{");
    {
        GeneratedCodeUtils::ImplIndentationScope indentScope(code);
        Q_UNUSED(indentScope);
        for (const QString &line : qAsConst(compiled.firstLines))
            code.appendToImpl(line);
        for (const QString &line : qAsConst(compiled.body))
            code.appendToImpl(line);
        for (const QString &line : qAsConst(compiled.lastLines))
            code.appendToImpl(line);
    }
    code.appendToImpl(u"}");
}

void CodeGeneratorWriter::writeUrl(GeneratedCodeUtils &code, const QQmlJSAotMethod &urlMethod)
{
    const auto [hSignature, _] = functionSignatures(urlMethod);
    Q_UNUSED(_);
    Q_ASSERT(!urlMethod.returnType.isEmpty());
    code.appendToImpl(functionReturnType(urlMethod) + hSignature);
    code.appendToImpl(u"{");
    {
        GeneratedCodeUtils::ImplIndentationScope indentScope(code);
        Q_UNUSED(indentScope);
        Q_ASSERT(urlMethod.firstLines.isEmpty() && urlMethod.lastLines.isEmpty());
        for (const QString &line : qAsConst(urlMethod.body))
            code.appendToImpl(line);
    }
    code.appendToImpl(u"}");
}

void CodeGeneratorWriter::write(GeneratedCodeUtils &code, const QQmlJSProgram &compiled)
{
    writeGlobalHeader(code, compiled.url, compiled.hPath, compiled.cppPath, compiled.outNamespace,
                      compiled.includes);

    code.appendToImpl(u""); // just new line
    writeUrl(code, compiled.urlMethod);

    // forward declare objects before writing them
    for (const QQmlJSAotObject &compiled : qAsConst(compiled.compiledObjects))
        code.appendToHeader(u"class " + compiled.cppType + u";");

    // write all the objects
    for (const QQmlJSAotObject &compiled : qAsConst(compiled.compiledObjects))
        write(code, compiled);

    writeGlobalFooter(code, compiled.url, compiled.hPath, compiled.cppPath, compiled.outNamespace);
}
