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

#include "qqmltcpropertyutils_p.h"

#include <private/qqmltcvisitor_p.h>

#include <QtCore/qfileinfo.h>
#include <QtCore/qstack.h>
#include <QtCore/qdir.h>
#include <QtCore/qloggingcategory.h>
#include <QtQml/private/qqmlsignalnames_p.h>

#include <private/qqmljsutils_p.h>

#include <algorithm>
#include <array>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

Q_LOGGING_CATEGORY(lcQmltcCompiler, "qml.qmltc.compiler", QtWarningMsg);

namespace QQmltc {

QSpan<const QmltcLoggingCategoryOverride> categoryOverrides()
{
    const auto Error = std::make_optional(QtCriticalMsg);

    // We want qmltc to be stricter than qmllint. Override certain categories.
    // Kept in sync via tst_qmltc::ensureWarningCategoryOverridesSync
    static const std::array overrides{
        QmltcLoggingCategoryOverride{ qmlAccessSingleton, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlAliasCycle, Error },
        QmltcLoggingCategoryOverride{ qmlAssignmentInCondition, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlAttachedPropertyReuse, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlComma, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlCompiler, Error },
        QmltcLoggingCategoryOverride{ qmlComponentChildrenCount, Error },
        QmltcLoggingCategoryOverride{ qmlConfusingExpressionStatement, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlConfusingMinuses, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlConfusingPluses, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlContextProperties, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlDeferredPropertyId, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlEnumsAreNotTypes, Error },
        QmltcLoggingCategoryOverride{ qmlEqualityTypeCoercion, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlDeprecated, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlDuplicateEnumEntries, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlDuplicateImport, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlDuplicateInlineComponent, Error },
        QmltcLoggingCategoryOverride{ qmlDuplicatePropertyBinding, Error },
        QmltcLoggingCategoryOverride{ qmlDuplicatedName, Error },
        QmltcLoggingCategoryOverride{ qmlEnumEntryMatchesEnum, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlEval, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlFunctionUsedBeforeDeclaration, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlImport, Error },
        QmltcLoggingCategoryOverride{ qmlImportFileSelector, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlIncompatibleType, Error },
        QmltcLoggingCategoryOverride{ qmlInheritanceCycle, Error },
        QmltcLoggingCategoryOverride{ qmlInvalidLintDirective, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlLiteralConstructor, Error },
        QmltcLoggingCategoryOverride{ qmlMissingEnumEntry, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlMissingProperty, Error },
        QmltcLoggingCategoryOverride{ qmlMissingType, Error },
        QmltcLoggingCategoryOverride{ qmlMultilineStrings, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlNonListProperty, Error },
        QmltcLoggingCategoryOverride{ qmlNonRootEnums, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlPropertyOverride, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlUnterminatedCase, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlPlugin, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlPreferNonVarProperties, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlPrefixedImportType, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlReadOnlyProperty, Error },
        QmltcLoggingCategoryOverride{ qmlRecursionDepthErrors, Error },
        QmltcLoggingCategoryOverride{ qmlRedundantOptionalChaining, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlRequired, Error },
        QmltcLoggingCategoryOverride{ qmlShadow, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlSignalParameters, Error },
        QmltcLoggingCategoryOverride{ qmlStalePropertyRead, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlSyntax, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlSyntaxDuplicateIds, Error },
        QmltcLoggingCategoryOverride{ qmlSyntaxIdQuotation, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlTopLevelComponent, Error },
        QmltcLoggingCategoryOverride{ qmlTranslationFunctionMismatch, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlUncreatableType, Error },
        QmltcLoggingCategoryOverride{ qmlUnintentionalEmptyBlock, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlUnqualified, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlUnreachableCode, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlUnresolvedAlias, Error },
        QmltcLoggingCategoryOverride{ qmlUnresolvedType, Error },
        QmltcLoggingCategoryOverride{ qmlUnusedImports, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlUseProperFunction, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlVarUsedBeforeDeclaration, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlVoid, std::nullopt },
        QmltcLoggingCategoryOverride{ qmlWith, std::nullopt },
    };

    return overrides;
}

static QString uniqueNameFromPieces(const QStringList &pieces, QHash<QString, int> &repetitions)
{
    QString possibleName = pieces.join(u'_');
    const int count = repetitions[possibleName]++;
    if (count > 0)
        possibleName.append(u"_" + QString::number(count));
    return possibleName;
}

static bool isExplicitComponent(const QQmlJSScope::ConstPtr &type)
{
    if (!type->isComposite())
        return false;
    auto base = type->baseType();
    return base && base->internalName() == u"QQmlComponent";
}

/*! \internal
    Returns if type is an implicit component.
    This method should only be called after implicit components
    are detected, that is, after QQmlJSImportVisitor::endVisit(UiProgram *)
    was called.
 */
static bool isImplicitComponent(const QQmlJSScope::ConstPtr &type)
{
    // root components and inline components are explicitly components.
    if (!type->isComposite() || type->isFileRootComponent() || type->isInlineComponent())
        return false;

    const auto cppBase = QQmlJSScope::nonCompositeBaseType(type);
    const bool isComponentBased = (cppBase && cppBase->internalName() == u"QQmlComponent");
    return type->componentRootStatus() != QQmlJSScope::IsComponentRoot::No && !isComponentBased;
}

/*! \internal
    Checks if type is inside or a (implicit or explicit) component.
    This method should only be called after implicit components
    are detected, that is, after QQmlJSImportVisitor::endVisit(UiProgram *)
    was called.
 */
static bool isOrUnderComponent(QQmlJSScope::ConstPtr type)
{
    Q_ASSERT(type->isComposite()); // we're dealing with composite types here
    for (; type; type = type->parentScope()) {
        if (isExplicitComponent(type) || isImplicitComponent(type)) {
            return true;
        }
    }
    return false;
}

Visitor::Visitor(QQmlJSImporter *importer, QQmlJSLogger *logger,
                 const QString &implicitImportDirectory, const QStringList &qmldirFiles)
    : QQmlJSImportVisitor(importer, logger, implicitImportDirectory, qmldirFiles)
{
    m_qmlTypeNames.append(QFileInfo(logger->filePath()).baseName()); // put document root
}

void Visitor::findCppIncludes()
{
    // TODO: this pass is slow: we have to do exhaustive search because some C++
    // code could do forward declarations and they are hard to handle correctly
    QSet<const QQmlJSScope *> visitedTypes; // we can still improve by walking all types only once
    const auto visitType = [&visitedTypes](const QQmlJSScope::ConstPtr &type) -> bool {
        if (visitedTypes.contains(type.data()))
            return true;
        visitedTypes.insert(type.data());
        return false;
    };
    const auto addCppInclude = [this](const QQmlJSScope::ConstPtr &type) {
        if (QString includeFile = filePath(type); !includeFile.isEmpty())
            m_cppIncludes.insert(std::move(includeFile));
    };

    const auto findInType = [&](const QQmlJSScope::ConstPtr &type) {
        if (!type)
            return;
        if (visitType(type)) // optimization - don't call nonCompositeBaseType() needlessly
            return;

        // look in type
        addCppInclude(type);

        if (type->isListProperty())
            addCppInclude(type->elementType());

        // look in type's base type
        auto base = type->baseType();
        if (!base && type->isComposite())
            // in this case, qqmljsimportvisitor would have already print an error message
            // about the missing type, so just return silently without crashing
            return;
        if (!base || visitType(base))
            return;
        addCppInclude(base);
    };

    const auto constructPrivateInclude = [](QStringView publicInclude) -> QString {
        if (publicInclude.isEmpty())
            return QString();
        Q_ASSERT(publicInclude.endsWith(u".h"_s) || publicInclude.endsWith(u".hpp"_s));
        const qsizetype dotLocation = publicInclude.lastIndexOf(u'.');
        QStringView extension = publicInclude.sliced(dotLocation);
        QStringView includeWithoutExtension = publicInclude.first(dotLocation);
        // check if "public" include is in fact already private
        if (publicInclude.startsWith(u"private"))
            return includeWithoutExtension + u"_p" + extension;
        return u"private/" + includeWithoutExtension + u"_p" + extension;
    };

    // walk the whole type hierarchy
    QStack<QQmlJSScope::ConstPtr> types;
    types.push(m_exportedRootScope);
    while (!types.isEmpty()) {
        auto type = types.pop();
        Q_ASSERT(type);

        const auto scopeType = type->scopeType();
        if (scopeType != QQmlSA::ScopeType::QMLScope
            && scopeType != QQmlSA::ScopeType::GroupedPropertyScope
            && scopeType != QQmlSA::ScopeType::AttachedPropertyScope) {
            continue;
        }

        for (auto t = type; !type->isArrayScope() && t; t = t->baseType()) {
            findInType(t);

            // look in properties
            const auto properties = t->ownProperties();
            for (const QQmlJSMetaProperty &p : properties) {
                findInType(p.type());

                if (p.isPrivate()) {
                    const QString ownersInclude = filePath(t);
                    QString privateInclude = constructPrivateInclude(ownersInclude);
                    if (!privateInclude.isEmpty())
                        m_cppIncludes.insert(std::move(privateInclude));
                }
            }

            // look in methods
            const auto methods = t->ownMethods();
            for (const QQmlJSMetaMethod &m : methods) {
                findInType(m.returnType());

                const auto parameters = m.parameters();
                for (const auto &param : parameters)
                    findInType(param.type());
            }
        }

        types.append(type->childScopes());
    }

    // remove own include
    m_cppIncludes.remove(filePath(m_exportedRootScope));
}

static void addCleanQmlTypeName(QStringList *names, const QQmlJSScope::ConstPtr &scope)
{
    Q_ASSERT(scope->scopeType() == QQmlSA::ScopeType::QMLScope);
    Q_ASSERT(!scope->isArrayScope());
    Q_ASSERT(!scope->baseTypeName().isEmpty());
    // the scope is guaranteed to be a new QML type, so any prefixes (separated
    // by dots) should be import namespaces
    const std::optional<QString> &inlineComponentName = scope->inlineComponentName();
    QString name = inlineComponentName ? *inlineComponentName : scope->baseTypeName();
    names->append(name.replace(u'.', u'_'));
}

bool Visitor::visit(QQmlJS::AST::UiObjectDefinition *object)
{
    if (!QQmlJSImportVisitor::visit(object))
        return false;

    // we're not interested in non-QML scopes
    if (m_currentScope->scopeType() != QQmlSA::ScopeType::QMLScope)
        return true;

    if (m_currentScope->isInlineComponent()) {
        m_inlineComponentNames.append(m_currentRootName);
        m_inlineComponents[m_currentRootName] = m_currentScope;
    }

    if (m_currentScope != m_exportedRootScope) // not document root
        addCleanQmlTypeName(&m_qmlTypeNames, m_currentScope);
    // give C++-relevant internal names to QMLScopes, we can use them later in compiler
    m_currentScope->setInternalName(uniqueNameFromPieces(m_qmlTypeNames, m_qmlTypeNameCounts));
    m_qmlTypesWithQmlBases[m_currentRootName].append(m_currentScope);

    return true;
}

void Visitor::endVisit(QQmlJS::AST::UiObjectDefinition *object)
{
    if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope)
        m_qmlTypeNames.removeLast();
    QQmlJSImportVisitor::endVisit(object);
}

bool Visitor::visit(QQmlJS::AST::UiObjectBinding *uiob)
{
    if (!QQmlJSImportVisitor::visit(uiob))
        return false;

    if (m_currentScope != m_exportedRootScope) // not document root
        addCleanQmlTypeName(&m_qmlTypeNames, m_currentScope);
    // give C++-relevant internal names to QMLScopes, we can use them later in compiler
    m_currentScope->setInternalName(uniqueNameFromPieces(m_qmlTypeNames, m_qmlTypeNameCounts));

    m_qmlTypesWithQmlBases[m_currentRootName].append(m_currentScope);
    return true;
}

void Visitor::endVisit(QQmlJS::AST::UiObjectBinding *uiob)
{
    m_qmlTypeNames.removeLast();
    QQmlJSImportVisitor::endVisit(uiob);
}

bool Visitor::visit(QQmlJS::AST::UiPublicMember *publicMember)
{
    if (!QQmlJSImportVisitor::visit(publicMember))
        return false;

    // augment property: set its write/read/etc. methods
    if (publicMember->type == QQmlJS::AST::UiPublicMember::Property) {
        const auto name = publicMember->name.toString();

        QQmlJSScope::Ptr owner =
                m_savedBindingOuterScope ? m_savedBindingOuterScope : m_currentScope;
        QQmlJSMetaProperty property = owner->ownProperty(name);
        Q_ASSERT(property.isValid());
        if (!property.isAlias()) { // aliases are covered separately
            PropertyData compiledData(property);
            if (property.read().isEmpty())
                property.setRead(compiledData.read);
            if (!property.isList()) {
                if (property.write().isEmpty() && property.isWritable())
                    property.setWrite(compiledData.write);
                // Note: prefer BINDABLE to NOTIFY
                if (property.bindable().isEmpty())
                    property.setBindable(compiledData.bindable);
            }
            owner->addOwnProperty(property);
        }

        const QString notifyName = QQmlSignalNames::propertyNameToChangedSignalName(name);
        // also check that notify is already a method of the scope
        {
            auto owningScope = m_savedBindingOuterScope ? m_savedBindingOuterScope : m_currentScope;
            const auto methods = owningScope->ownMethods(notifyName);
            if (methods.size() != 1) {
                const QString errorString =
                        methods.isEmpty() ? u"no signal"_s : u"too many signals"_s;
                m_logger->log(
                        u"internal error: %1 found for property '%2'"_s.arg(errorString, name),
                        qmlCompiler, publicMember->identifierToken);
                return false;
            } else if (methods[0].methodType() != QQmlJSMetaMethodType::Signal) {
                m_logger->log(u"internal error: method %1 of property %2 must be a signal"_s.arg(
                                      notifyName, name),
                              qmlCompiler, publicMember->identifierToken);
                return false;
            }
        }
    }

    return true;
}

bool Visitor::visit(QQmlJS::AST::UiScriptBinding *scriptBinding)
{
    if (!QQmlJSImportVisitor::visit(scriptBinding))
        return false;

    {
        const auto id = scriptBinding->qualifiedId;
        if (!id->next && id->name == QLatin1String("id"))
            m_typesWithId[m_currentScope] = -1; // temporary value
    }

    return true;
}

bool Visitor::visit(QQmlJS::AST::UiInlineComponent *component)
{
    if (!QQmlJSImportVisitor::visit(component))
        return false;
    return true;
}

void Visitor::endVisit(QQmlJS::AST::UiProgram *program)
{
    QQmlJSImportVisitor::endVisit(program);
    if (!rootScopeIsValid()) // in case we failed badly
        return;

    QHash<QQmlJSScope::ConstPtr, QList<QQmlJSMetaPropertyBinding>> bindings;

    // Yes, we want absolutely all bindings in the document.
    // Not only the ones immediately assigned to QML types.
    for (const QQmlJSScope::ConstPtr &type : std::as_const(m_scopesByIrLocation))
        bindings.insert(type, type->ownPropertyBindingsInQmlIROrder());

    postVisitResolve(bindings);
    setupAliases();

    if (m_mode != Mode::Compile)
        return;

    findCppIncludes();

    for (const QList<QQmlJSScope::ConstPtr> &qmlTypes : m_pureQmlTypes)
        for (const QQmlJSScope::ConstPtr &type : qmlTypes)
            checkNamesAndTypes(type);
}

bool Visitor::checkCustomParser(const QQmlJSScope::ConstPtr &scope)
{
    if (QQmlJSImportVisitor::checkCustomParser(scope))
        m_seenCustomParsers = true;
    return false;
}

QQmlJSScope::ConstPtr fetchType(const QQmlJSMetaPropertyBinding &binding)
{
    switch (binding.bindingType()) {
    case QQmlSA::BindingType::Object:
        return binding.objectType();
    case QQmlSA::BindingType::Interceptor:
        return binding.interceptorType();
    case QQmlSA::BindingType::ValueSource:
        return binding.valueSourceType();
    case QQmlSA::BindingType::AttachedProperty:
        return binding.attachedType();
    case QQmlSA::BindingType::GroupProperty:
        return binding.groupType();
    default:
        return {};
    }
    Q_UNREACHABLE_RETURN({});
}

template<typename TypePredicate, typename BindingPredicate>
void iterateBindings(
        const QQmlJSScope::ConstPtr &root,
        const QHash<QQmlJSScope::ConstPtr, QList<QQmlJSMetaPropertyBinding>> &qmlIrOrderedBindings,
        TypePredicate typePredicate, BindingPredicate bindingPredicate)
{
    // NB: depth-first-search is used here to mimic various QmlIR passes
    QStack<QQmlJSScope::ConstPtr> types;
    types.push(root);
    while (!types.isEmpty()) {
        auto current = types.pop();

        if (typePredicate(current))
            continue;

        Q_ASSERT(qmlIrOrderedBindings.contains(current));
        const auto &bindings = qmlIrOrderedBindings[current];
        // reverse the binding order here, because stack processes right-most
        // child first and we need left-most first
        for (auto it = bindings.rbegin(); it != bindings.rend(); ++it) {
            const auto &binding = *it;

            if (bindingPredicate(current, binding))
                continue;

            if (auto type = fetchType(binding))
                types.push(type);
        }
    }
}

template<typename Predicate>
void iterateTypes(
        const QQmlJSScope::ConstPtr &root,
        const QHash<QQmlJSScope::ConstPtr, QList<QQmlJSMetaPropertyBinding>> &qmlIrOrderedBindings,
        Predicate predicate)
{
    iterateBindings(root, qmlIrOrderedBindings, [predicate](const QQmlJSScope::ConstPtr &current) {
        return predicate(current) || isOrUnderComponent(current);
    }, [](const QQmlJSScope::ConstPtr &, const QQmlJSMetaPropertyBinding &) {
        return false;
    });
}

/*! \internal
    This is a special function that must be called after
    QQmlJSImportVisitor::endVisit(QQmlJS::AST::UiProgram *). It is used to
    resolve things that couldn't be resolved during the AST traversal, such
    as anything that is dependent on implicit or explicit components
*/
void Visitor::postVisitResolve(
        const QHash<QQmlJSScope::ConstPtr, QList<QQmlJSMetaPropertyBinding>> &qmlIrOrderedBindings)
{

    // add the document root (that is not an inline component), as we usually
    // want to iterate on all inline components, followed by the document root
    m_inlineComponentNames.append(RootDocumentNameType());
    m_inlineComponents[RootDocumentNameType()] = m_exportedRootScope;

    // match scopes to indices of QmlIR::Object from QmlIR::Document
    qsizetype count = 0;
    const auto setIndex = [&](const QQmlJSScope::Ptr &current) {
        if (current->scopeType() != QQmlSA::ScopeType::QMLScope || current->isArrayScope())
            return;
        Q_ASSERT(!m_qmlIrObjectIndices.contains(current));
        m_qmlIrObjectIndices[current] = count;
        ++count;
    };
    QQmlJSUtils::traverseFollowingQmlIrObjectStructure(m_exportedRootScope, setIndex);

    // find types that are part of the deferred bindings (we care about *types*
    // exclusively here)
    QSet<QQmlJSScope::ConstPtr> deferredTypes;
    const auto findDeferred = [&](const QQmlJSScope::ConstPtr &type,
                                  const QQmlJSMetaPropertyBinding &binding) {
        const QString propertyName = binding.propertyName();
        Q_ASSERT(!propertyName.isEmpty());
        if (type->isNameDeferred(propertyName)) {
            m_typesWithDeferredBindings.insert(type);

            if (binding.hasObject() || binding.hasInterceptor() || binding.hasValueSource()) {
                deferredTypes.insert(fetchType(binding));
                return true;
            }
        }
        return false;
    };
    for (const auto &inlineComponentName : m_inlineComponentNames) {
        iterateBindings(m_inlineComponents[inlineComponentName], qmlIrOrderedBindings,
                        isOrUnderComponent, findDeferred);
    }

    const auto isOrUnderDeferred = [&deferredTypes](QQmlJSScope::ConstPtr type) {
        for (; type; type = type->parentScope()) {
            if (deferredTypes.contains(type))
                return true;
        }
        return false;
    };

    // find all "pure" QML types
    QList<QQmlJSScope::ConstPtr> explicitComponents;
    for (qsizetype i = 0; i < m_qmlTypes.size(); ++i) {
        const QQmlJSScope::ConstPtr &type = m_qmlTypes.at(i);

        if (isOrUnderComponent(type) || isOrUnderDeferred(type)) {
            // root is special: we compile Component roots. root is also never
            // deferred, so in case `isOrUnderDeferred(type)` returns true, we
            // always continue here
            if (type != m_exportedRootScope) {
                // if a type is an explicit component, its id "leaks" into the
                // document context
                if (isExplicitComponent(type))
                    explicitComponents.append(type);
                continue;
            }
        }

        const InlineComponentOrDocumentRootName inlineComponent =
                type->enclosingInlineComponentName();
        QList<QQmlJSScope::ConstPtr> &pureQmlTypes = m_pureQmlTypes[inlineComponent];
        m_creationIndices[type] = pureQmlTypes.size();
        pureQmlTypes.append(type);
    }

    // update the typeCounts
    for (const auto &inlineComponent : m_inlineComponentNames) {
        m_inlineComponentTypeCount[inlineComponent] = m_pureQmlTypes[inlineComponent].size();
    }

    // add explicit components to the object creation indices
    {
        QHash<InlineComponentOrDocumentRootName, qsizetype> index;
        for (const QQmlJSScope::ConstPtr &c : std::as_const(explicitComponents)) {
            const InlineComponentOrDocumentRootName inlineComponent =
                    c->enclosingInlineComponentName();
            m_creationIndices[c] =
                    m_pureQmlTypes[inlineComponent].size() + index[inlineComponent]++;
            m_inlineComponentTypeCount[inlineComponent]++;
        }
    }

    // m_qmlTypesWithQmlBases should contain the types to be compiled.
    // Some types should not be compiled and are therefore filtered out:
    //  * deferred types
    //  * types inside of capital-c-Components (implicit and explicit)
    //  * non-composite types (that is, types not defined in qml)
    //
    // This can not be done earlier as implicitly wrapped Components are
    // only known after visitation is over!

    // filter step:
    for (const auto &inlineComponent : m_inlineComponentNames) {
        QList<QQmlJSScope::ConstPtr> filteredQmlTypesWithQmlBases;
        QList<QQmlJSScope::ConstPtr> &unfilteredQmlTypesWithQmlBases =
                m_qmlTypesWithQmlBases[inlineComponent];
        filteredQmlTypesWithQmlBases.reserve(unfilteredQmlTypesWithQmlBases.size());
        std::copy_if(unfilteredQmlTypesWithQmlBases.cbegin(), unfilteredQmlTypesWithQmlBases.cend(),
                     std::back_inserter(filteredQmlTypesWithQmlBases),
                     [&](const QQmlJSScope::ConstPtr &type) {
                         auto base = type->baseType();
                         return base && base->isComposite() && !isOrUnderComponent(type)
                                 && !isOrUnderDeferred(type);
                     });
        qSwap(unfilteredQmlTypesWithQmlBases, filteredQmlTypesWithQmlBases);
    }

    // count QmlIR::Objects in the document - the amount is used to calculate
    // object indices of implicit components
    QHash<InlineComponentOrDocumentRootName, qsizetype> qmlScopeCount;
    const auto countQmlScopes = [&](const QQmlJSScope::ConstPtr &scope) {
        if (scope->isArrayScope()) // special kind of QQmlJSScope::QMLScope
            return;
        switch (scope->scopeType()) {
        case QQmlSA::ScopeType::QMLScope:
        case QQmlSA::ScopeType::GroupedPropertyScope:
        case QQmlSA::ScopeType::AttachedPropertyScope: {
            ++qmlScopeCount[scope->enclosingInlineComponentName()];
            break;
        }
        default:
            return;
        }
        return;
    };
    // order doesn't matter (so re-use QQmlJSUtils)
    QQmlJSUtils::traverseFollowingQmlIrObjectStructure(m_exportedRootScope, countQmlScopes);

    // figure synthetic indices of QQmlComponent-wrapped types
    int syntheticCreationIndex;
    const auto addSyntheticIndex = [&](const QQmlJSScope::ConstPtr &type) {
        // explicit component
        if (isExplicitComponent(type)) {
            m_syntheticTypeIndices[type] = m_qmlIrObjectIndices.value(type, -1);
            return true;
        }
        // implicit component
        if (isImplicitComponent(type)) {
            const int index =
                    qmlScopeCount[type->enclosingInlineComponentName()] + syntheticCreationIndex++;
            m_syntheticTypeIndices[type] = index;
            return true;
        }
        return false;
    };

    for (const auto &inlineComponentName : m_inlineComponentNames) {
        syntheticCreationIndex = 0; // reset for each inline component
        iterateTypes(m_inlineComponents[inlineComponentName], qmlIrOrderedBindings,
                     addSyntheticIndex);
    }

    // figure runtime object ids for non-component wrapped types
    int currentId;
    const auto setRuntimeId = [&](const QQmlJSScope::ConstPtr &type) {
        // any type wrapped in an implicit component shouldn't be processed
        // here. even if it has id, it doesn't need to be set by qmltc
        if (type->isInlineComponent()) {
            // explicit inline component
        } else if (type->isFileRootComponent()) {
            // explicit root component
        } else if (type->componentRootStatus() != QQmlJSScope::IsComponentRoot::No) {
            // Wrapped in implicit component, assigned to unknown property, or child of scope
            // called "QQmlComponent". We consider this an "implicit component".
            return true;
        }

        if (m_typesWithId.contains(type)) {
            m_typesWithId[type] = currentId++;
        }

        return false;
    };

    for (const auto &inlineComponentName : m_inlineComponentNames) {
        currentId = 0; // reset for each inline component
        iterateTypes(m_inlineComponents[inlineComponentName], qmlIrOrderedBindings, setRuntimeId);
    }
}

static void setAliasData(QQmlJSMetaProperty *alias, const QQmlJSUtils::ResolvedAlias &origin)
{
    Q_ASSERT(origin.kind != QQmlJSUtils::AliasTarget_Invalid);
    PropertyData compiledData(*alias);
    if (alias->read().isEmpty())
        alias->setRead(compiledData.read);
    if (origin.kind == QQmlJSUtils::AliasTarget_Object) // id-pointing aliases only have READ method
        return;
    if (origin.property.isWritable() && alias->write().isEmpty())
        alias->setWrite(compiledData.write);

    // the engine always compiles a notify for properties/aliases defined in qml code
    // Yes, this generated notify will never be emitted.
    if (alias->notify().isEmpty())
        alias->setNotify(compiledData.notify);

    if (!origin.property.bindable().isEmpty() && alias->bindable().isEmpty())
        alias->setBindable(compiledData.bindable);
}

void Visitor::setupAliases()
{
    QStack<QQmlJSScope::Ptr> types;
    types.push(m_exportedRootScope);

    while (!types.isEmpty()) {
        QQmlJSScope::Ptr current = types.pop();
        auto properties = current->ownProperties();

        for (QQmlJSMetaProperty &p : properties) {
            if (!p.isAlias())
                continue;

            auto result = QQmlJSUtils::resolveAlias(m_scopesById, p, current,
                                                    QQmlJSUtils::AliasResolutionVisitor {});
            if (result.kind == QQmlJSUtils::AliasTarget_Invalid) {
                m_logger->log(QStringLiteral("Cannot resolve alias \"%1\"").arg(p.propertyName()),
                              qmlUnresolvedAlias, current->sourceLocation());
                continue;
            }
            setAliasData(&p, result);
            current->addOwnProperty(p);
        }
    }
}

void Visitor::checkNamesAndTypes(const QQmlJSScope::ConstPtr &type)
{
    static const QString cppKeywords[] = {
        u"alignas"_s,
        u"alignof"_s,
        u"and"_s,
        u"and_eq"_s,
        u"asm"_s,
        u"atomic_cancel"_s,
        u"atomic_commit"_s,
        u"atomic_noexcept"_s,
        u"auto"_s,
        u"bitand"_s,
        u"bitor"_s,
        u"bool"_s,
        u"break"_s,
        u"case"_s,
        u"catch"_s,
        u"char"_s,
        u"char16_t"_s,
        u"char32_t"_s,
        u"char8_t"_s,
        u"class"_s,
        u"co_await"_s,
        u"co_return"_s,
        u"co_yield"_s,
        u"compl"_s,
        u"concept"_s,
        u"const"_s,
        u"const_cast"_s,
        u"consteval"_s,
        u"constexpr"_s,
        u"constinit"_s,
        u"continue"_s,
        u"decltype"_s,
        u"default"_s,
        u"delete"_s,
        u"do"_s,
        u"double"_s,
        u"dynamic_cast"_s,
        u"else"_s,
        u"enum"_s,
        u"explicit"_s,
        u"export"_s,
        u"extern"_s,
        u"false"_s,
        u"float"_s,
        u"for"_s,
        u"friend"_s,
        u"goto"_s,
        u"if"_s,
        u"inline"_s,
        u"int"_s,
        u"long"_s,
        u"mutable"_s,
        u"namespace"_s,
        u"new"_s,
        u"noexcept"_s,
        u"not"_s,
        u"not_eq"_s,
        u"nullptr"_s,
        u"operator"_s,
        u"or"_s,
        u"or_eq"_s,
        u"private"_s,
        u"protected"_s,
        u"public"_s,
        u"reflexpr"_s,
        u"register"_s,
        u"reinterpret_cast"_s,
        u"requires"_s,
        u"return"_s,
        u"short"_s,
        u"signed"_s,
        u"sizeof"_s,
        u"static"_s,
        u"static_assert"_s,
        u"static_cast"_s,
        u"struct"_s,
        u"switch"_s,
        u"synchronized"_s,
        u"template"_s,
        u"this"_s,
        u"thread_local"_s,
        u"throw"_s,
        u"true"_s,
        u"try"_s,
        u"typedef"_s,
        u"typeid"_s,
        u"typename"_s,
        u"union"_s,
        u"unsigned"_s,
        u"using"_s,
        u"virtual"_s,
        u"void"_s,
        u"volatile"_s,
        u"wchar_t"_s,
        u"while"_s,
        u"xor"_s,
        u"xor_eq"_s,
    };
    Q_ASSERT(std::is_sorted(std::begin(cppKeywords), std::end(cppKeywords)));

    const auto isReserved = [&](QStringView word) {
        if (word.startsWith(QChar(u'_')) && word.size() >= 2
            && (word[1].isUpper() || word[1] == QChar(u'_'))) {
            return true; // Identifiers starting with underscore and uppercase are reserved in C++
        }
        return std::binary_search(std::begin(cppKeywords), std::end(cppKeywords), word);
    };

    const auto validate = [&](QStringView name, QStringView errorPrefix) {
        if (!isReserved(name))
            return;
        m_logger->log(errorPrefix + u" '" + name + u"' is a reserved C++ word, consider renaming",
                      qmlCompiler, type->sourceLocation());
    };

    const auto validateType = [&type, this](const QQmlJSScope::ConstPtr &typeToCheck,
                                            QStringView name, QStringView errorPrefix) {
        if (typeToCheck.isNull()) {
            m_logger->log(
                    QStringLiteral(
                            "Can't compile the %1 type \"%2\" to C++ because it cannot be resolved")
                            .arg(errorPrefix, name),
                    qmlCompiler, type->sourceLocation());
            return;
        }

        if (type->moduleName().isEmpty())
            return;

        if (typeToCheck->isComposite() && typeToCheck->moduleName() != type->moduleName()) {
            m_logger->log(
                    QStringLiteral(
                            "Can't compile the %1 type \"%2\" to C++ because it "
                            "lives in \"%3\" instead of the current file's \"%4\" QML module.")
                            .arg(errorPrefix, name, typeToCheck->moduleName(), type->moduleName()),
                    qmlCompiler, type->sourceLocation());
        }
    };

    validateType(type->baseType(), type->baseTypeName(), u"QML base");

    const auto enums = type->ownEnumerations();
    for (auto it = enums.cbegin(); it != enums.cend(); ++it) {
        const QQmlJSMetaEnum e = it.value();
        validate(e.name(), u"Enumeration");

        const auto enumKeys = e.keys();
        for (const auto &key : enumKeys)
            validate(key, u"Enumeration '%1' key"_s.arg(e.name()));
    }

    const auto properties = type->ownProperties();
    for (auto it = properties.cbegin(); it != properties.cend(); ++it) {
        const QQmlJSMetaProperty &p = it.value();
        validate(p.propertyName(), u"Property");

        if (!p.isAlias() && !p.typeName().isEmpty())
            validateType(p.type(), p.typeName(), u"QML property");
    }

    const auto methods = type->ownMethods();
    for (auto it = methods.cbegin(); it != methods.cend(); ++it) {
        const QQmlJSMetaMethod &m = it.value();
        validate(m.methodName(), u"Method");
        if (!m.returnTypeName().isEmpty())
            validateType(m.returnType(), m.returnTypeName(), u"QML method return");

        for (const auto &parameter : m.parameters()) {
            validate(parameter.name(), u"Method '%1' parameter"_s.arg(m.methodName()));
            if (!parameter.typeName().isEmpty())
                validateType(parameter.type(), parameter.typeName(), u"QML parameter");
        }
    }

    // TODO: one could also test signal handlers' parameters but we do not store
    // this information in QQmlJSMetaPropertyBinding currently
}

/*! \internal
 *  Returns the file path for the C++ header of \a scope or the header created
 *  by qmltc for it and its inline components.
 */
QString Visitor::filePath(const QQmlJSScope::ConstPtr &scope) const
{
    const QString filePath = scope->filePath();
    if (!filePath.endsWith(u".qml")) // assume the correct path is set
        return scope->filePath();

    const QString correctedFilePath = sourceDirectoryPath(filePath);
    const QStringList paths = m_importer->resourceFileMapper()->resourcePaths(
            QQmlJSResourceFileMapper::localFileFilter(correctedFilePath));
    auto firstHeader = std::find_if(paths.cbegin(), paths.cend(),
                                    [](const QString &x) { return x.endsWith(u".h"_s); });
    if (firstHeader == paths.cend()) {
        const QString matchedPaths = paths.isEmpty() ? u"<none>"_s : paths.join(u", ");
        qCDebug(lcQmltcCompiler,
                "Failed to find a header file name for path %s. Paths checked:\n%s",
                correctedFilePath.toUtf8().constData(), matchedPaths.toUtf8().constData());
        return QString();
    }
    // NB: get the file name to avoid prefixes
    return QFileInfo(*firstHeader).fileName();
}

QString Visitor::sourceDirectoryPath(const QString &path) const
{
    auto result = QQmlJSUtils::sourceDirectoryPath(m_importer, path);
    if (const QString *srcDirPath = std::get_if<QString>(&result))
        return *srcDirPath;

    const QQmlJS::DiagnosticMessage *error = std::get_if<QQmlJS::DiagnosticMessage>(&result);
    Q_ASSERT(error);
    qCDebug(lcQmltcCompiler, "%s", error->message.toUtf8().constData());
    // return input as a fallback
    return path;
}

} // namespace QQmltc

QT_END_NAMESPACE
