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

#include "qmljsfindexportedcpptypes.h"

#include "qmljsdocument.h"
#include "qmljsinterpreter.h"
#include "qmljstr.h"

#include <cplusplus/Overview.h>
#include <cplusplus/TypeOfExpression.h>
#include <cplusplus/cppmodelmanagerbase.h>
#include <cplusplus/CppDocument.h>
#include <utils/qtcassert.h>

#include <QList>
#include <QRegularExpression>

//using namespace QmlJS;

namespace {
using namespace CPlusPlus;

class ExportedQmlType {
public:
    QString packageName;
    QString typeName;
    LanguageUtils::ComponentVersion version;
    Scope *scope;
    QString typeExpression;
    QString url;
    bool isCreatable;
    bool isSingleton;
};

class ContextProperty {
public:
    QString name;
    QString expression;
    int line, column;
};

class FindExportsVisitor : protected ASTVisitor
{
    CPlusPlus::Document::Ptr _doc;
    QList<ExportedQmlType> _exportedTypes;
    QList<ContextProperty> _contextProperties;
    CompoundStatementAST *_compound;
    ASTMatcher _matcher;
    ASTPatternBuilder _builder;
    Overview _overview;
    QList<CPlusPlus::Document::DiagnosticMessage> _messages;

public:
    enum RegistrationFunction {
        InvalidRegistrationFunction,
        // template<typename T> int qmlRegisterType(const char *uri, int versionMajor, int versionMinor, const char *qmlName);
        // or template<typename T, int metaObjectRevision> int qmlRegisterType(const char *uri, int versionMajor, int versionMinor, const char *qmlName);
        QmlRegisterType4,
        // int qmlRegisterType(const QUrl & url, const char * uri, int versionMajor, int versionMinor, const char * qmlName)
        QmlRegisterType5,
        // template<typename T> inline auto qmlRegisterSingletonInstance(const char *uri, int versionMajor, int versionMinor, const char *typeName, T *cppObject)
        QmlRegisterSingletonInstance,
        // template<typename T> int qmlRegisterSingletonType(const char * uri, int versionMajor, int versionMinor, const char * typeName, QObject *(* ) ( QQmlEngine *, QJSEngine * ) callback)
        QmlRegisterSingletonTypeCallback1,
        // int qmlRegisterSingletonType(const char * uri, int versionMajor, int versionMinor, const char * typeName, QJSValue(* ) ( QQmlEngine *, QJSEngine * ) callback)
        QmlRegisterSingletonTypeCallback2,
        // int qmlRegisterSingletonType(const QUrl &url, const char *uri, int versionMajor, int versionMinor, const char *qmlName)
        QmlRegisterSingletonTypeUrl,
        // template<typename T> int qmlRegisterUncreatableType(const char *uri, int versionMajor, int versionMinor, const char *qmlName, const QString& reason)
        // or template<typename T, int metaObjectRevision> int qmlRegisterUncreatableType(const char *uri, int versionMajor, int versionMinor, const char *qmlName, const QString& reason)
        QmlRegisterUncreatableType,
        // int qmlRegisterUncreatableMetaObject(const QMetaObject &staticMetaObject, const char *uri, int versionMajor, int versionMinor, const char *qmlName, const QString &reason)
        QmlRegisterUncreatableMetaObject
    };

    FindExportsVisitor(CPlusPlus::Document::Ptr doc)
        : ASTVisitor(doc->translationUnit())
        , _doc(doc)
        , _compound(nullptr)
    {}

    void operator()()
    {
        _exportedTypes.clear();
        _contextProperties.clear();
        accept(translationUnit()->ast());
    }

    QList<CPlusPlus::Document::DiagnosticMessage> messages() const
    {
        return _messages;
    }

    QList<ExportedQmlType> exportedTypes() const
    {
        return _exportedTypes;
    }

    QList<ContextProperty> contextProperties() const
    {
        return _contextProperties;
    }

protected:
    bool visit(CompoundStatementAST *ast) override
    {
        CompoundStatementAST *old = _compound;
        _compound = ast;
        accept(ast->statement_list);
        _compound = old;
        return false;
    }

    bool visit(CallAST *ast) override
    {
        if (checkForQmlRegisterType(ast))
            return false;
        checkForSetContextProperty(ast);
        return false;
    }

    bool checkForQmlRegisterType(CallAST *ast)
    {
        IdExpressionAST *idExp = ast->base_expression->asIdExpression();
        if (!idExp || !idExp->name)
            return false;
        RegistrationFunction registrationFunction = InvalidRegistrationFunction;
        TypeIdAST *typeId = nullptr;
        if (TemplateIdAST *templateId = idExp->name->asTemplateId()) {
            if (!templateId->identifier_token)
                return false;

            // check the name
            const Identifier *templateIdentifier = translationUnit()->identifier(templateId->identifier_token);
            if (!templateIdentifier)
                return false;
            const QByteArray callName(templateIdentifier->chars());
            if (callName == "qmlRegisterType")
                registrationFunction = QmlRegisterType4;
            else if (callName == "qmlRegisterSingletonInstance")
                registrationFunction = QmlRegisterSingletonInstance;
            else if (callName == "qmlRegisterSingletonType")
                registrationFunction = QmlRegisterSingletonTypeCallback1;
            else if (callName == "qmlRegisterUncreatableType")
                registrationFunction = QmlRegisterUncreatableType;
            else
                return false;

            // check that there is a typeId
            if (!templateId->template_argument_list || !templateId->template_argument_list->value)
                return false;
            // sometimes there can be a second argument, the metaRevisionNumber
            if (templateId->template_argument_list->next) {
                if (!templateId->template_argument_list->next->value ||
                        templateId->template_argument_list->next->next)
                    return false;
                // should just check for a generic ExpressionAST?
                NumericLiteralAST *metaRevision =
                        templateId->template_argument_list->next->value->asNumericLiteral();
                if (!metaRevision)
                    return false;
            }

            typeId = templateId->template_argument_list->value->asTypeId();
            if (!typeId)
                return false;
        } else if (idExp->name) {
            QByteArray fName;
            if (SimpleNameAST *simpleName = idExp->name->asSimpleName()) {
                if (const Identifier *id = translationUnit()->identifier(simpleName->identifier_token))
                    fName = QByteArray(id->chars(), id->size());
            }
            if (fName == "qmlRegisterType")
                registrationFunction = QmlRegisterType5;
            else if (fName == "qmlRegisterSingletonInstance") // when called without explicit template parameter
                registrationFunction = QmlRegisterSingletonInstance;
            else if (fName == "qmlRegisterSingletonType")
                registrationFunction = QmlRegisterSingletonTypeCallback2;
            else if (fName == "qmlRegisterUncreatableMetaObject")
                registrationFunction = QmlRegisterUncreatableMetaObject;
            else
                return false;
        } else {
            return false;
        }

        int argCount = 0;
        for (const ExpressionListAST *list = ast->expression_list; list && list->value;
             list = list->next) {
            ++argCount;
        }

        // must have at least four arguments
        if (argCount < 4)
            return false;
        switch (registrationFunction) {
        case InvalidRegistrationFunction:
            return false;
        case QmlRegisterType4:
            break;
        case QmlRegisterType5:
        case QmlRegisterSingletonInstance:
        case QmlRegisterSingletonTypeCallback1:
        case QmlRegisterSingletonTypeCallback2:
        case QmlRegisterSingletonTypeUrl:
        case QmlRegisterUncreatableType:
            if (argCount != 5)
                return false;
            break;
        case QmlRegisterUncreatableMetaObject:
            if (argCount != 6)
                return false;
        }
        ExpressionAST *uriExp = nullptr;
        ExpressionAST *majorVersionExp = nullptr;
        ExpressionAST *minorVersionExp = nullptr;
        ExpressionAST *nameExp = nullptr;
        if (registrationFunction == QmlRegisterType5) {
            uriExp = ast->expression_list->next->value;
            majorVersionExp = ast->expression_list->next->next->value;
            minorVersionExp = ast->expression_list->next->next->next->value;
            nameExp = ast->expression_list->next->next->next->next->value;
        } else if (registrationFunction == QmlRegisterSingletonTypeCallback2
                   && (!ast->expression_list->next->value->asNumericLiteral()
                       || ast->expression_list->next->next->next->value->asNumericLiteral())) {
            // discriminate between QmlRegisterSingletonTypeCallback2 and QmlRegisterSingletonTypeUrl,
            // this is very rough check, improve?
            registrationFunction = QmlRegisterSingletonTypeUrl;
            uriExp = ast->expression_list->next->value;
            majorVersionExp = ast->expression_list->next->next->value;
            minorVersionExp = ast->expression_list->next->next->next->value;
            nameExp = ast->expression_list->next->next->next->next->value;
        } else if (registrationFunction == QmlRegisterUncreatableMetaObject) {
            uriExp = ast->expression_list->next->value;
            majorVersionExp = ast->expression_list->next->next->value;
            minorVersionExp = ast->expression_list->next->next->next->value;
            nameExp = ast->expression_list->next->next->next->next->value;
        } else {
            uriExp = ast->expression_list->value;
            majorVersionExp = ast->expression_list->next->value;
            minorVersionExp = ast->expression_list->next->next->value;
            nameExp = ast->expression_list->next->next->next->value;
        }
        const StringLiteral *nameLit = nullptr;
        if (StringLiteralAST *nameAst = skipStringCall(nameExp)->asStringLiteral())
            nameLit = translationUnit()->stringLiteral(nameAst->literal_token);
        if (!nameLit) {
            int line, column;
            translationUnit()->getTokenPosition(nameExp->firstToken(), &line, &column);
            _messages += Document::DiagnosticMessage(
                        Document::DiagnosticMessage::Warning,
                        _doc->filePath(),
                        line, column,
                        QmlJS::Tr::tr(
                            "The type will only be available in the QML editors when the type name is a string literal."));
            return false;
        }

        // if the uri is a string literal, things are easy
        QString packageName;
        if (StringLiteralAST *packageAst = skipStringCall(uriExp)->asStringLiteral()) {
            const StringLiteral *packageLit = translationUnit()->stringLiteral(packageAst->literal_token);
            packageName = QString::fromUtf8(packageLit->chars(), packageLit->size());
        }
        // as a special case, allow an identifier package argument if there's a
        // Q_ASSERT(QLatin1String(uri) == QLatin1String("actual uri"));
        // in the enclosing compound statement
        QString uriNameString;
        if (IdExpressionAST *uriName = uriExp->asIdExpression())
            uriNameString = stringOf(uriName);
        if (packageName.isEmpty() && !uriNameString.isEmpty() && _compound) {
            for (StatementListAST *it = _compound->statement_list; it; it = it->next) {
                StatementAST *stmt = it->value;

                packageName = nameOfUriAssert(stmt, uriNameString);
                if (!packageName.isEmpty())
                    break;
            }
        }
        if (packageName.isEmpty() && _compound) {
            // check the comments in _compound for annotations
            const QRegularExpression uriAnnotation(QLatin1String("@uri\\s*([\\w\\.]*)"));

            // scan every comment between the pipes in
            // {|
            //    // comment
            //    othercode;
            //   |qmlRegisterType<Foo>(...);
            const Token begin = _doc->translationUnit()->tokenAt(_compound->firstToken());
            const Token end = _doc->translationUnit()->tokenAt(ast->firstToken());

            // go through comments backwards to find the annotation closest to the call
            for (int i = _doc->translationUnit()->commentCount(); i-- > 0; ) {
                const Token commentToken = _doc->translationUnit()->commentAt(i);
                if (commentToken.utf16charsBegin() >= end.utf16charsBegin()
                        || commentToken.utf16charsEnd() <= begin.utf16charsBegin()) {
                    continue;
                }
                const QString comment = stringOf(commentToken);
                const QRegularExpressionMatch match = uriAnnotation.match(comment);
                if (match.hasMatch()) {
                    packageName = match.captured(1);
                    break;
                }
            }
        }
        if (packageName.isEmpty()) {
            packageName = QmlJS::CppQmlTypes::defaultPackage;
            int line, column;
            translationUnit()->getTokenPosition(ast->firstToken(), &line, &column);
            _messages += Document::DiagnosticMessage(
                        Document::DiagnosticMessage::Warning,
                        _doc->filePath(),
                        line, column,
                        QmlJS::Tr::tr(
                            "The module URI cannot be determined by static analysis. The type will not be available\n"
                            "globally in the QML editor. You can add a \"// @uri My.Module.Uri\" annotation to let\n"
                            "the QML editor know about a likely URI."));
        }

        // version arguments must be integer literals
        const NumericLiteral *majorLit = nullptr;
        const NumericLiteral *minorLit = nullptr;
        if (NumericLiteralAST *majorAst = majorVersionExp->asNumericLiteral())
            majorLit = translationUnit()->numericLiteral(majorAst->literal_token);
        if (NumericLiteralAST *minorAst = minorVersionExp->asNumericLiteral())
            minorLit = translationUnit()->numericLiteral(minorAst->literal_token);

        // build the descriptor
        ExportedQmlType exportedType;
        exportedType.isSingleton = registrationFunction == QmlRegisterSingletonInstance
                || registrationFunction == QmlRegisterSingletonTypeCallback1
                || registrationFunction == QmlRegisterSingletonTypeCallback2
                || registrationFunction == QmlRegisterSingletonTypeUrl;
        exportedType.isCreatable = !exportedType.isSingleton
                && registrationFunction != QmlRegisterUncreatableType
                && registrationFunction != QmlRegisterUncreatableMetaObject;
        exportedType.typeName = QString::fromUtf8(nameLit->chars(), nameLit->size());
        exportedType.packageName = packageName;
        if (majorLit && minorLit && majorLit->isInt() && minorLit->isInt()) {
            exportedType.version = LanguageUtils::ComponentVersion(
                        QString::fromUtf8(majorLit->chars(), majorLit->size()).toInt(),
                        QString::fromUtf8(minorLit->chars(), minorLit->size()).toInt());
        }

        // we want to do lookup later, so also store the surrounding scope
        int line, column;
        translationUnit()->getTokenPosition(ast->firstToken(), &line, &column);
        exportedType.scope = _doc->scopeAt(line, column);

        if (typeId){
            // add the expression
            const Token begin = translationUnit()->tokenAt(typeId->firstToken());
            const Token last = translationUnit()->tokenAt(typeId->lastToken() - 1);
            exportedType.typeExpression = QString::fromUtf8(
                        _doc->utf8Source().mid(begin.bytesBegin(), last.bytesEnd() - begin.bytesBegin()));
        } else if (registrationFunction == QmlRegisterSingletonTypeCallback2) {
            // we cannot really do much with generated QJSValue values
        } else if (registrationFunction == QmlRegisterSingletonTypeUrl
                   || registrationFunction == QmlRegisterType5) {
            // try to recover QUrl("literal")
            if (ast->expression_list && ast->expression_list->value) {
                ExpressionAST *urlAst = skipStringCall(skipQUrl(ast->expression_list->value, translationUnit()));
                if (StringLiteralAST *uriAst = urlAst->asStringLiteral()) {
                    const StringLiteral *urlLit = translationUnit()->stringLiteral(uriAst->literal_token);
                    exportedType.url = QString::fromUtf8(urlLit->chars(), urlLit->size());
                }
            }
        } else if (registrationFunction == QmlRegisterUncreatableMetaObject) {
            // Anything to do here?
        } else {
            qCWarning(QmlJS::qmljsLog()) << "missing template type for registrationFunction " << registrationFunction;
        }
        _exportedTypes += exportedType;

        return true;
    }

    static NameAST *callName(ExpressionAST *exp)
    {
        if (IdExpressionAST *idExp = exp->asIdExpression())
            return idExp->name;
        if (MemberAccessAST *memberExp = exp->asMemberAccess())
            return memberExp->member_name;
        return nullptr;
    }

    static ExpressionAST *skipQVariant(ExpressionAST *ast, TranslationUnit *translationUnit)
    {
        CallAST *call = ast->asCall();
        if (!call)
            return ast;
        if (!call->expression_list
                || !call->expression_list->value
                || call->expression_list->next)
            return ast;

        IdExpressionAST *idExp = call->base_expression->asIdExpression();
        if (!idExp || !idExp->name)
            return ast;

        // QVariant(foo) -> foo
        if (SimpleNameAST *simpleName = idExp->name->asSimpleName()) {
            const Identifier *id = translationUnit->identifier(simpleName->identifier_token);
            if (!id)
                return ast;
            if (QString::fromUtf8(id->chars(), id->size()) != QLatin1String("QVariant"))
                return ast;
            return call->expression_list->value;
        // QVariant::fromValue(foo) -> foo
        } else if (QualifiedNameAST *q = idExp->name->asQualifiedName()) {
            SimpleNameAST *simpleRhsName = q->unqualified_name->asSimpleName();
            if (!simpleRhsName
                    || !q->nested_name_specifier_list
                    || !q->nested_name_specifier_list->value
                    || q->nested_name_specifier_list->next)
                return ast;
            const Identifier *rhsId = translationUnit->identifier(simpleRhsName->identifier_token);
            if (!rhsId)
                return ast;
            if (QString::fromUtf8(rhsId->chars(), rhsId->size()) != QLatin1String("fromValue"))
                return ast;
            NestedNameSpecifierAST *nested = q->nested_name_specifier_list->value;
            SimpleNameAST *simpleLhsName = nested->class_or_namespace_name->asSimpleName();
            if (!simpleLhsName)
                return ast;
            const Identifier *lhsId = translationUnit->identifier(simpleLhsName->identifier_token);
            if (!lhsId)
                return ast;
            if (QString::fromUtf8(lhsId->chars(), lhsId->size()) != QLatin1String("QVariant"))
                return ast;
            return call->expression_list->value;
        }

        return ast;
    }

    static ExpressionAST *skipQUrl(ExpressionAST *ast, TranslationUnit *translationUnit)
    {
        CallAST *call = ast->asCall();
        if (!call)
            return ast;
        if (!call->expression_list
                || !call->expression_list->value
                || call->expression_list->next)
            return ast;

        IdExpressionAST *idExp = call->base_expression->asIdExpression();
        if (!idExp || !idExp->name)
            return ast;

        // QUrl(foo) -> foo
        if (SimpleNameAST *simpleName = idExp->name->asSimpleName()) {
            const Identifier *id = translationUnit->identifier(simpleName->identifier_token);
            if (!id)
                return ast;
            if (QByteArray(id->chars(), id->size()) == "QUrl")
                return call->expression_list->value;
        }

        return ast;
    }

    bool checkForSetContextProperty(CallAST *ast)
    {
        // check whether ast->base_expression has the 'setContextProperty' name
        NameAST *callNameAst = callName(ast->base_expression);
        if (!callNameAst)
            return false;
        SimpleNameAST *simpleNameAst = callNameAst->asSimpleName();
        if (!simpleNameAst || !simpleNameAst->identifier_token)
            return false;
        const Identifier *nameIdentifier = translationUnit()->identifier(simpleNameAst->identifier_token);
        if (!nameIdentifier)
            return false;
        const QString callName = QString::fromUtf8(nameIdentifier->chars(), nameIdentifier->size());
        if (callName != QLatin1String("setContextProperty"))
            return false;

        // must have two arguments
        if (!ast->expression_list
                || !ast->expression_list->value || !ast->expression_list->next
                || !ast->expression_list->next->value || ast->expression_list->next->next)
            return false;

        // first argument must be a string literal
        const StringLiteral *nameLit = nullptr;
        if (StringLiteralAST *nameAst = skipStringCall(ast->expression_list->value)->asStringLiteral())
            nameLit = translationUnit()->stringLiteral(nameAst->literal_token);
        if (!nameLit) {
            int line, column;
            translationUnit()->getTokenPosition(ast->expression_list->value->firstToken(), &line, &column);
            _messages += Document::DiagnosticMessage(
                        Document::DiagnosticMessage::Warning,
                        _doc->filePath(),
                        line, column,
                        QmlJS::Tr::tr(
                            "must be a string literal to be available in the QML editor"));
            return false;
        }

        ContextProperty contextProperty;
        contextProperty.name = QString::fromUtf8(nameLit->chars(), nameLit->size());
        contextProperty.expression = stringOf(skipQVariant(ast->expression_list->next->value, translationUnit()));
        // we want to do lookup later, so also store the line and column of the target scope
        translationUnit()->getTokenPosition(ast->firstToken(),
                                            &contextProperty.line,
                                            &contextProperty.column);

        _contextProperties += contextProperty;

        return true;
    }

private:
    QString stringOf(CPlusPlus::AST *ast)
    {
        return stringOf(ast->firstToken(), ast->lastToken() - 1);
    }

    QString stringOf(int first, int last)
    {
        const Token firstToken = translationUnit()->tokenAt(first);
        const Token lastToken = translationUnit()->tokenAt(last);
        return QString::fromUtf8(
            _doc->utf8Source().mid(firstToken.bytesBegin(),
                                   lastToken.bytesEnd() - firstToken.bytesBegin()));
    }

    QString stringOf(const Token &token)
    {
        return QString::fromUtf8(_doc->utf8Source().mid(token.bytesBegin(), token.bytes()));
    }

    ExpressionAST *skipStringCall(ExpressionAST *exp)
    {
        if (!exp || !exp->asCall())
            return exp;

        IdExpressionAST *callName = _builder.IdExpression();
        CallAST *call = _builder.Call(callName);
        if (!exp->match(call, &_matcher))
            return exp;

        const QString name = stringOf(callName);
        if (name != QLatin1String("QLatin1String")
                && name != QLatin1String("QString"))
            return exp;

        if (!call->expression_list || call->expression_list->next)
            return exp;

        return call->expression_list->value;
    }

    QString nameOfUriAssert(StatementAST *stmt, const QString &uriName)
    {
        QString null;

        IdExpressionAST *outerCallName = _builder.IdExpression();
        BinaryExpressionAST *binary = _builder.BinaryExpression();
        // assert(... == ...);
        ExpressionStatementAST *pattern = _builder.ExpressionStatement(
                    _builder.Call(outerCallName, _builder.ExpressionList(
                                     binary)));

        if (!stmt->match(pattern, &_matcher)) {
            outerCallName = _builder.IdExpression();
            binary = _builder.BinaryExpression();
            // the expansion of Q_ASSERT(...),
            // ((!(... == ...)) ? qt_assert(...) : ...);
            pattern = _builder.ExpressionStatement(
                        _builder.NestedExpression(
                            _builder.ConditionalExpression(
                                _builder.NestedExpression(
                                    _builder.UnaryExpression(
                                        _builder.NestedExpression(
                                            binary))),
                                _builder.Call(outerCallName))));

            if (!stmt->match(pattern, &_matcher))
                return null;
        }

        const QString outerCall = stringOf(outerCallName);
        if (outerCall != QLatin1String("qt_assert")
                && outerCall != QLatin1String("assert")
                && outerCall != QLatin1String("Q_ASSERT"))
            return null;

        if (translationUnit()->tokenAt(binary->binary_op_token).kind() != T_EQUAL_EQUAL)
            return null;

        ExpressionAST *lhsExp = skipStringCall(binary->left_expression);
        ExpressionAST *rhsExp = skipStringCall(binary->right_expression);
        if (!lhsExp || !rhsExp)
            return null;

        StringLiteralAST *uriString = lhsExp->asStringLiteral();
        IdExpressionAST *uriArgName = lhsExp->asIdExpression();
        if (!uriString)
            uriString = rhsExp->asStringLiteral();
        if (!uriArgName)
            uriArgName = rhsExp->asIdExpression();
        if (!uriString || !uriArgName)
            return null;

        if (stringOf(uriArgName) != uriName)
            return null;

        const StringLiteral *packageLit = translationUnit()->stringLiteral(uriString->literal_token);
        return QString::fromUtf8(packageLit->chars(), packageLit->size());
    }
};

static FullySpecifiedType stripPointerAndReference(const FullySpecifiedType &type)
{
    Type *t = type.type();
    while (t) {
        if (PointerType *ptr = t->asPointerType())
            t = ptr->elementType().type();
        else if (ReferenceType *ref = t->asReferenceType())
            t = ref->elementType().type();
        else
            break;
    }
    return FullySpecifiedType(t);
}

static QString toQmlType(const FullySpecifiedType &type)
{
    Overview overview;
    QString result = overview.prettyType(stripPointerAndReference(type));
    if (result == QLatin1String("QString"))
        result = QLatin1String("string");
    return result;
}

static Class *lookupClass(const QString &expression, Scope *scope, TypeOfExpression &typeOf)
{
    const QList<LookupItem> results = typeOf(expression.toUtf8(), scope);
    Class *klass = nullptr;
    for (const LookupItem &item : results) {
        if (item.declaration()) {
            klass = item.declaration()->asClass();
            if (klass)
                return klass;
        }
    }
    return nullptr;
}

static LanguageUtils::FakeMetaObject::Ptr buildFakeMetaObject(
        Class *klass,
        QHash<Class *, LanguageUtils::FakeMetaObject::Ptr> *fakeMetaObjects,
        TypeOfExpression &typeOf)
{
    using namespace LanguageUtils;

    if (FakeMetaObject::Ptr fmo = fakeMetaObjects->value(klass))
        return fmo;

    FakeMetaObject::Ptr fmo(new FakeMetaObject);
    if (!klass)
        return fmo;
    fakeMetaObjects->insert(klass, fmo);

    Overview namePrinter;

    fmo->setClassName(namePrinter.prettyName(klass->name()));
    // add the no-package export, so the cpp name can be used in properties
    fmo->addExport(fmo->className(), QmlJS::CppQmlTypes::cppPackage, ComponentVersion());

    for (int i = 0; i < klass->memberCount(); ++i) {
        Symbol *member = klass->memberAt(i);
        if (!member->name())
            continue;
        if (Function *func = member->type()->asFunctionType()) {
            if (!func->isSlot() && !func->isInvokable() && !func->isSignal())
                continue;
            FakeMetaMethod method(namePrinter.prettyName(func->name()), toQmlType(func->returnType()));
            if (func->isSignal())
                method.setMethodType(FakeMetaMethod::Signal);
            else
                method.setMethodType(FakeMetaMethod::Slot);
            for (int a = 0, argc = func->argumentCount(); a < argc; ++a) {
                Symbol *arg = func->argumentAt(a);
                QString name;
                if (arg->name())
                    name = namePrinter.prettyName(arg->name());
                method.addParameter(name, toQmlType(arg->type()));
            }
            fmo->addMethod(method);
        }
        if (QtPropertyDeclaration *propDecl = member->asQtPropertyDeclaration()) {
            const FullySpecifiedType &type = propDecl->type();
            const bool isList = false; // ### fixme
            const bool isWritable = propDecl->flags() & QtPropertyDeclaration::WriteFunction;
            const bool isPointer = type.type() && type.type()->asPointerType();
            const int revision = 0; // ### fixme
            FakeMetaProperty property(
                        namePrinter.prettyName(propDecl->name()),
                        toQmlType(type),
                        isList, isWritable, isPointer,
                        revision);
            fmo->addProperty(property);
        }
        if (QtEnum *qtEnum = member->asQtEnum()) {
            // find the matching enum
            Enum *e = nullptr;
            const QList<LookupItem> result = typeOf(namePrinter.prettyName(qtEnum->name()).toUtf8(), klass);
            for (const LookupItem &item : result) {
                if (item.declaration()) {
                    e = item.declaration()->asEnum();
                    if (e)
                        break;
                }
            }
            if (!e)
                continue;

            FakeMetaEnum metaEnum(namePrinter.prettyName(e->name()));
            for (int j = 0; j < e->memberCount(); ++j) {
                Symbol *enumMember = e->memberAt(j);
                if (!enumMember->name())
                    continue;
                metaEnum.addKey(namePrinter.prettyName(enumMember->name()));
            }
            fmo->addEnum(metaEnum);
        }
    }

    // only single inheritance is supported
    if (klass->baseClassCount() > 0) {
        BaseClass *base = klass->baseClassAt(0);
        if (!base->name())
            return fmo;
        const QString baseClassName = namePrinter.prettyName(base->name());
        fmo->setSuperclassName(baseClassName);

        Class *baseClass = lookupClass(baseClassName, klass, typeOf);
        if (!baseClass)
            return fmo;
        buildFakeMetaObject(baseClass, fakeMetaObjects, typeOf);
    }

    return fmo;
}

static void buildExportedQmlObjects(
        TypeOfExpression &typeOf,
        const QList<ExportedQmlType> &cppExports,
        QHash<Class *, LanguageUtils::FakeMetaObject::Ptr> *fakeMetaObjects,
        QList<LanguageUtils::FakeMetaObject::Ptr> *extraFakeMetaObjects)
{
    using namespace LanguageUtils;

    if (cppExports.isEmpty())
        return;

    for (const ExportedQmlType &exportedType : cppExports) {
        Class *klass = nullptr;
        if (!exportedType.typeExpression.isEmpty())
            klass = lookupClass(exportedType.typeExpression, exportedType.scope, typeOf);
        // TODO: something smarter with exportedType.url
        // accepts a null klass
        FakeMetaObject::Ptr fmo = buildFakeMetaObject(klass, fakeMetaObjects, typeOf);
        fmo->addExport(exportedType.typeName,
                       exportedType.packageName,
                       exportedType.version);
        fmo->setIsCreatable(exportedType.isCreatable);
        fmo->setIsSingleton(exportedType.isSingleton);
        if (!klass) {
            fmo->setClassName(QLatin1String("__autogen__") + exportedType.typeName);
            extraFakeMetaObjects->append(fmo);
        }
    }
}

static void buildContextProperties(
        const Document::Ptr &doc,
        TypeOfExpression &typeOf,
        const QList<ContextProperty> &contextPropertyDescriptions,
        QHash<Class *, LanguageUtils::FakeMetaObject::Ptr> *fakeMetaObjects,
        QHash<QString, QString> *contextProperties)
{
    using namespace LanguageUtils;

    for (const ContextProperty &property : contextPropertyDescriptions) {
        Scope *scope = doc->scopeAt(property.line, property.column);
        QList<LookupItem> results = typeOf(property.expression.toUtf8(), scope);
        QString typeName;
        if (!results.isEmpty()) {
            LookupItem result = results.first();
            FullySpecifiedType simpleType = stripPointerAndReference(result.type());
            if (NamedType *namedType = simpleType.type()->asNamedType()) {
                Scope *typeScope = result.scope();
                if (!typeScope)
                    typeScope = scope; // incorrect but may be an ok fallback
                ClassOrNamespace *binding = typeOf.context().lookupType(namedType->name(), typeScope);
                if (binding && !binding->symbols().isEmpty()) {
                    // find the best 'Class' symbol
                    for (int i = binding->symbols().size() - 1; i >= 0; --i) {
                        if (Class *klass = binding->symbols().at(i)->asClass()) {
                            FakeMetaObject::Ptr fmo = buildFakeMetaObject(klass, fakeMetaObjects, typeOf);
                            typeName = fmo->className();
                            break;
                        }
                    }
                }
            }
        }

        contextProperties->insert(property.name, typeName);
    }
}

} // anonymous namespace

namespace QmlJS {

FindExportedCppTypes::FindExportedCppTypes(const CPlusPlus::Snapshot &snapshot)
    : m_snapshot(snapshot)
{
}

QStringList FindExportedCppTypes::operator()(const CPlusPlus::Document::Ptr &document)
{
    QTC_ASSERT(!document.isNull(), return {});

    m_contextProperties.clear();
    m_exportedTypes.clear();
    QStringList fileNames;

    // this check only guards against some input errors, if document's source and AST has not
    // been guarded properly the source and AST may still become empty/null while this function is running
    if (document->utf8Source().isEmpty()
            || !document->translationUnit()->ast())
        return fileNames;

    FindExportsVisitor finder(document);
    finder();
    static const QString kindKey = QLatin1String("QmlJSTools.ExportedQmlTypesDiagnostic");
    CppModelManagerBase::trySetExtraDiagnostics(document->filePath(), kindKey, finder.messages());

    // if nothing was found, done
    const QList<ContextProperty> contextPropertyDescriptions = finder.contextProperties();
    const QList<ExportedQmlType> exports = finder.exportedTypes();
    if (exports.isEmpty() && contextPropertyDescriptions.isEmpty())
        return fileNames;

    // context properties need lookup inside function scope, and thus require a full check
    CPlusPlus::Document::Ptr localDoc = document;
    if (document->checkMode() != CPlusPlus::Document::FullCheck
            && !contextPropertyDescriptions.isEmpty()) {
        localDoc = m_snapshot.documentFromSource(document->utf8Source(), document->filePath());
        localDoc->check();
    }

    // create a single type of expression (and bindings) for the document
    TypeOfExpression typeOf;
    typeOf.init(localDoc, m_snapshot);
    QHash<Class *, LanguageUtils::FakeMetaObject::Ptr> fakeMetaObjects;
    QList<LanguageUtils::FakeMetaObject::Ptr> extraFakeMetaObjects;

    // generate the exports from qmlRegisterType
    buildExportedQmlObjects(typeOf, exports, &fakeMetaObjects, &extraFakeMetaObjects);

    // add the types from the context properties and create a name->cppname map
    // also expose types where necessary
    buildContextProperties(localDoc, typeOf, contextPropertyDescriptions,
                           &fakeMetaObjects, &m_contextProperties);

    // convert to list of FakeMetaObject::ConstPtr
    m_exportedTypes.reserve(fakeMetaObjects.size() + extraFakeMetaObjects.size());
    fileNames.reserve(fakeMetaObjects.size());
    for (auto it = fakeMetaObjects.constBegin(), end = fakeMetaObjects.constEnd(); it != end;
         ++it) {
        it.value()->updateFingerprint();
        m_exportedTypes += it.value();
        fileNames += QLatin1String(it.key()->fileName());
    }
    for (const LanguageUtils::FakeMetaObject::Ptr &fmo : std::as_const(extraFakeMetaObjects)) {
        fmo->updateFingerprint();
        m_exportedTypes += fmo;
    }

    return fileNames;
}

QList<LanguageUtils::FakeMetaObject::ConstPtr> FindExportedCppTypes::exportedTypes() const
{
    return m_exportedTypes;
}

QHash<QString, QString> FindExportedCppTypes::contextProperties() const
{
    return m_contextProperties;
}

bool FindExportedCppTypes::maybeExportsTypes(const CPlusPlus::Document::Ptr &document)
{
    if (!document->control())
        return false;
    const QByteArray tokens[] = {
        "qmlRegisterSingletonInstance",
        "qmlRegisterSingletonType",
        "qmlRegisterType",
        "qmlRegisterUncreatableType",
        "setContextProperty"
        "qmlRegisterUncreatableMetaObject",
    };
    for (const QByteArray &token : tokens) {
        if (document->control()->findIdentifier(token.constData(), token.size())) {
            return true;
        }
    }
    return false;
}

} // namespace QmlJS
