// Copyright (C) 2020 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 "qmetatypesjsonprocessor_p.h"

#include "qanystringviewutils_p.h"
#include "qqmltyperegistrarconstants_p.h"
#include "qqmltyperegistrarutils_p.h"
#include "qqmltypesclassdescription_p.h"
#include "qqmltyperegistrarutils_p.h"

#include <QtCore/qcborarray.h>
#include <QtCore/qcbormap.h>
#include <QtCore/qdir.h>
#include <QtCore/qfile.h>
#include <QtCore/qjsondocument.h>
#include <QtCore/qqueue.h>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;
using namespace Constants;
using namespace Constants::MetatypesDotJson;
using namespace Constants::MetatypesDotJson::Qml;
using namespace QAnyStringViewUtils;

const MetaTypePrivate MetaType::s_empty;

// TODO: This could be optimized to store the objects in a more compact way.
std::vector<std::unique_ptr<MetaTypePrivate>> s_pool;

static QCborValue fromJson(const QByteArray &json, QJsonParseError *error)
{
    const QJsonDocument jsonValue = QJsonDocument::fromJson(json, error);
    if (jsonValue.isArray())
        return QCborValue::fromJsonValue(jsonValue.array());
    if (jsonValue.isObject())
        return QCborValue::fromJsonValue(jsonValue.object());
    return QCborValue();
}

QList<QAnyStringView> MetaTypesJsonProcessor::namespaces(const MetaType &classDef)
{
    const QAnyStringView unqualified = classDef.className();
    const QAnyStringView qualified = classDef.qualifiedClassName();

    QList<QAnyStringView> namespaces;
    if (qualified != unqualified) {
        namespaces = split(qualified, "::"_L1);
        Q_ASSERT(namespaces.last() == unqualified);
        namespaces.pop_back();
    }

    return namespaces;
}

bool MetaTypesJsonProcessor::processTypes(const QStringList &files)
{
    for (const QString &source: files) {
        if (m_seenMetaTypesFiles.hasSeen(QDir::cleanPath(source)))
            continue;

        QCborValue metaObjects;
        {
            QFile f(source);
            if (!f.open(QIODevice::ReadOnly)) {
                error(source) << "Cannot open file for reading";
                return false;
            }
            QJsonParseError parseError = {0, QJsonParseError::NoError};
            metaObjects = fromJson(f.readAll(), &parseError);
            if (parseError.error != QJsonParseError::NoError) {
                error(source)
                        << "Failed to parse JSON:" << parseError.error
                        << parseError.errorString();
                return false;
            }
        }

        if (metaObjects.isArray()) {
            const QCborArray metaObjectsArray = metaObjects.toArray();
            for (const QCborValue &metaObject : metaObjectsArray) {
                if (!metaObject.isMap()) {
                    error(source) <<  "JSON is not an object";
                    return false;
                }

                processTypes(metaObject.toMap());
            }
        } else if (metaObjects.isMap()) {
            processTypes(metaObjects.toMap());
        } else {
            error(source) << "JSON is not an object or an array";
            return false;
        }
    }

    return true;
}

bool MetaTypesJsonProcessor::processForeignTypes(const QString &types)
{
    QFile typesFile(types);
    if (!typesFile.open(QIODevice::ReadOnly)) {
        error(types) << "Cannot open foreign types file";
        return false;
    }

    QJsonParseError parseError = {0, QJsonParseError::NoError};
    QCborValue foreignMetaObjects = fromJson(typesFile.readAll(), &parseError);
    if (parseError.error != QJsonParseError::NoError) {
        error(types)
                << "Failed to parse JSON:" << parseError.error
                << parseError.errorString();
        return false;
    }

    const QCborArray foreignObjectsArray = foreignMetaObjects.toArray();
    for (const QCborValue &metaObject : foreignObjectsArray) {
        if (!metaObject.isMap()) {
            error(types) <<  "JSON is not an object";
            return false;
        }

        processForeignTypes(metaObject.toMap());
    }

    return true;
}

bool MetaTypesJsonProcessor::processForeignTypes(const QStringList &foreignTypesFiles)
{
    bool success = true;

    for (const QString &types : foreignTypesFiles) {
        if (m_seenMetaTypesFiles.hasSeen(QDir::cleanPath(types)))
            continue;

        if (!processForeignTypes(types))
            success = false;
    }
    return success;
}

template<typename String>
static void sortStringList(QList<String> *list)
{
    std::sort(list->begin(), list->end());
    const auto newEnd = std::unique(list->begin(), list->end());
    list->erase(typename QList<String>::const_iterator(newEnd), list->constEnd());
}

void MetaTypesJsonProcessor::postProcessTypes()
{
    sortTypes(m_types);
}

void MetaTypesJsonProcessor::postProcessForeignTypes()
{
    sortTypes(m_foreignTypes);
    sortStringList(&m_primitiveTypes);
    sortStringList(&m_usingDeclarations);
    addRelatedTypes();
    sortStringList(&m_referencedTypes);
    sortStringList(&m_includes);
}

QString MetaTypesJsonProcessor::extractRegisteredTypes() const
{
    QString registrationHelper;
    for (const auto &obj: m_types) {
        const QString className = obj.className().toString();
        const QString qualifiedClassName = obj.qualifiedClassName().toString();
        const QString foreignClassName = className + u"Foreign";
        QStringList qmlElements;
        QString qmlUncreatable;
        QString qmlAttached;
        bool isSingleton = false;
        bool isExplicitlyUncreatable = false;
        bool isNamespace = obj.kind() == MetaType::Kind::Namespace;
        for (const ClassInfo &entry: obj.classInfos()) {
            const auto name = entry.name;
            const auto value = entry.value;
            if (name == S_ELEMENT) {
                if (value == S_AUTO) {
                    qmlElements.append(u"QML_NAMED_ELEMENT("_s + className + u")"_s);
                } else if (value == S_ANONYMOUS) {
                    qmlElements.append(u"QML_ANONYMOUS"_s);
                } else {
                    qmlElements.append(u"QML_NAMED_ELEMENT("_s + value.toString() + u")");
                }
            } else if (name == S_CREATABLE && value == S_FALSE) {
                isExplicitlyUncreatable = true;
            } else if (name == S_UNCREATABLE_REASON) {
                qmlUncreatable = u"QML_UNCREATABLE(\""_s + value.toString() + u"\")";
            } else if (name == S_ATTACHED) {
                qmlAttached = u"QML_ATTACHED("_s + value.toString() + u")";
            } else if (name == S_SINGLETON) {
                isSingleton = true;
            }
        }
        if (qmlElements.isEmpty())
            continue; // no relevant entries found
        const QString spaces = u"    "_s;
        if (isNamespace) {
            registrationHelper += u"\nnamespace "_s + foreignClassName + u"{\n    Q_NAMESPACE\n"_s;
            registrationHelper += spaces + u"QML_FOREIGN_NAMESPACE(" + qualifiedClassName + u")\n"_s;
        } else {
            registrationHelper += u"\nstruct "_s + foreignClassName + u"{\n    Q_GADGET\n"_s;
            registrationHelper += spaces + u"QML_FOREIGN(" + qualifiedClassName + u")\n"_s;
        }
        registrationHelper += spaces + qmlElements.join(u"\n"_s) + u"\n"_s;
        if (isSingleton)
            registrationHelper += spaces + u"QML_SINGLETON\n"_s;
        if (isExplicitlyUncreatable) {
            if (qmlUncreatable.isEmpty())
                registrationHelper += spaces + uR"(QML_UNCREATABLE(""))" + u"n";
            else
                registrationHelper += spaces + qmlUncreatable + u"\n";
        }
        if (!qmlAttached.isEmpty())
            registrationHelper += spaces + qmlAttached + u"\n";
        registrationHelper += u"}";
        if (!isNamespace)
            registrationHelper += u";";
        registrationHelper += u"\n";
    }
    return registrationHelper;
}

MetaTypesJsonProcessor::PreProcessResult MetaTypesJsonProcessor::preProcess(
        const MetaType &classDef, PopulateMode populateMode)
{
    // If this type is a self-extending value type or a sequence type or has a JavaScript extension
    // and is not the root object, then it's foreign type has no entry of its own.
    // In that case we need to generate a "primitive" entry.

    QList<QAnyStringView> primitiveAliases;
    UsingDeclaration usingDeclaration;

    RegistrationMode mode = NoRegistration;
    bool isSelfExtendingValueType = false;
    bool hasJavaScriptExtension = false;
    bool isRootObject = false;
    bool isSequence = false;

    for (const ClassInfo &classInfo : classDef.classInfos()) {
        if (classInfo.name == S_FOREIGN)
            usingDeclaration.alias = classInfo.value;
        else if (classInfo.name == S_PRIMITIVE_ALIAS)
            primitiveAliases.append(classInfo.value);
        else if (classInfo.name == S_EXTENSION_IS_JAVA_SCRIPT)
            hasJavaScriptExtension = (classInfo.value == S_TRUE);
        else if (classInfo.name == S_EXTENDED && classDef.kind() == MetaType::Kind::Gadget)
            isSelfExtendingValueType = classInfo.value == classDef.className();
        else if (classInfo.name == S_ROOT)
            isRootObject = (classInfo.value == S_TRUE);
        else if (classInfo.name == S_SEQUENCE)
            isSequence = true;
        else if (classInfo.name == S_USING)
            usingDeclaration.original = classInfo.value;
        else if (populateMode == PopulateMode::Yes && classInfo.name == S_ELEMENT) {
            switch (classDef.kind()) {
            case MetaType::Kind::Object:
                mode = ObjectRegistration;
                break;
            case MetaType::Kind::Gadget:
                mode = GadgetRegistration;
                break;
            case MetaType::Kind::Namespace:
                mode = NamespaceRegistration;
                break;
            default:
                warning(classDef)
                     << "Not registering a classInfo which is neither an object,"
                     << "nor a gadget, nor a namespace:"
                     << classInfo.name.toString();
                break;
            }
        }
    }

    return PreProcessResult {
        std::move(primitiveAliases),
        usingDeclaration,
        (!isRootObject && (isSequence || isSelfExtendingValueType || hasJavaScriptExtension))
                ? usingDeclaration.alias
                : QAnyStringView(),
        mode
    };

}

// TODO: Remove this when QAnyStringView gets a proper qHash()
static size_t qHash(QAnyStringView string, size_t seed = 0)
{
    return string.visit([seed](auto view) {
        if constexpr (std::is_same_v<decltype(view), QStringView>)
            return qHash(view, seed);
        if constexpr (std::is_same_v<decltype(view), QLatin1StringView>)
            return qHash(view, seed);
        if constexpr (std::is_same_v<decltype(view), QUtf8StringView>)
            return qHash(QByteArrayView(view.data(), view.length()), seed);
    });
}

static bool qualifiedClassNameLessThan(const MetaType &a, const MetaType &b)
{
    return a.qualifiedClassName() < b.qualifiedClassName();
}

enum RemoveResult : bool { Removed, NotFound };
static RemoveResult removeFromSortedMetaTypeList(QList<MetaType> &typeList, const MetaType &toBeRemoved)
{
    const auto remove = std::equal_range(
            typeList.constBegin(), typeList.constEnd(), toBeRemoved,
            qualifiedClassNameLessThan);
    if (remove.first == remove.second)
        return NotFound; // type used in two non-opaque usages; e.g. Base + Extension
    for (auto it = remove.first; it != remove.second; ++it) {
        if (*it == toBeRemoved) {
            typeList.erase(it);
            return Removed;
        }
    }
    return NotFound;
}

// inserts toBeAdded if it's not in the list
static void insertIntoSortedMetaTypeList(QList<MetaType> &typeList, const MetaType &toBeAdded)
{
    const auto [lower, upper] = std::equal_range(
            typeList.constBegin(), typeList.constEnd(), toBeAdded,
            qualifiedClassNameLessThan);
    if (lower == upper)
        typeList.insert(lower, toBeAdded);
}

static bool sortedMetaTypeListContains(const QList<MetaType> &typeList, const MetaType &type)
{
    const auto [first, last] = std::equal_range(
            typeList.constBegin(), typeList.constEnd(), type, qualifiedClassNameLessThan);
    for (auto it = first; it != last; ++it) {
        if (*it == type)
            return true;
    }
    return false;
}

enum class TypeRelation
{
    Base, Property, Argument, Return, Enum, Attached, SequenceValue, Extension
};

// Whether the type whose related types are currently being processed is itself opaque.
enum class ContextOpacity
{
    FullyRegistered, Opaque
};

static QLatin1StringView typeRelationString(TypeRelation relation)
{
    switch (relation) {
    case TypeRelation::Property:      return "property"_L1;
    case TypeRelation::Argument:      return "argument"_L1;
    case TypeRelation::Return:        return "return"_L1;
    case TypeRelation::Enum:          return "enum"_L1;
    case TypeRelation::Attached:      return "attached"_L1;
    case TypeRelation::SequenceValue: return "sequence value"_L1;
    case TypeRelation::Extension:     return "extension"_L1;
    default:
        break;
    }

    Q_UNREACHABLE_RETURN(QLatin1StringView());
}

void MetaTypesJsonProcessor::addRelatedTypes()
{
    QSet<QAnyStringView> processedRelatedNativeNames;
    QSet<QAnyStringView> processedRelatedJavaScriptNames;
    QSet<QAnyStringView> unresolvedForeignNames;
    QQueue<MetaType> typeQueue;
    typeQueue.append(m_types);

    const auto addRelatedName
            = [&](QAnyStringView relatedName, const QList<QAnyStringView> &namespaces) {
        if (const FoundType related = QmlTypesClassDescription::findType(
                    m_types, m_foreignTypes, relatedName, namespaces)) {

            if (!related.javaScript.isEmpty())
                processedRelatedJavaScriptNames.insert(related.javaScript.qualifiedClassName());

            if (!related.native.isEmpty())
                processedRelatedNativeNames.insert(related.native.qualifiedClassName());

            return true;
        } else {
            return false;
        }
    };

    const auto addRelatedType = [&](const MetaType &type) {
        const QAnyStringView qualifiedName = type.qualifiedClassName();
        if (type.inputFile().isEmpty())
            processedRelatedJavaScriptNames.insert(qualifiedName);
        else
            processedRelatedNativeNames.insert(qualifiedName);
    };

    // First mark all classes registered from this module as already processed.
    for (const MetaType &type : std::as_const(m_types)) {
        addRelatedType(type);
        for (const ClassInfo &obj : type.classInfos()) {
            if (obj.name == S_FOREIGN) {
                const QAnyStringView foreign = obj.value;
                if (!addRelatedName(foreign, namespaces(type)))
                    unresolvedForeignNames.insert(foreign);
                break;
            }
        }
    }

    // Then mark all classes registered from other modules as already processed.
    // We don't want to generate them again for this module.
    for (const MetaType &foreignType : std::as_const(m_foreignTypes)) {
        bool seenQmlPrefix = false;
        for (const ClassInfo &obj : foreignType.classInfos()) {
            const QAnyStringView name = obj.name;
            if (!seenQmlPrefix && startsWith(name, "QML."_L1)) {
                addRelatedType(foreignType);
                seenQmlPrefix = true;
            }
            if (name == S_FOREIGN
                    || name == S_EXTENDED
                    || name == S_ATTACHED
                    || name == S_SEQUENCE) {
                ResolvedTypeAlias foreign(obj.value, m_usingDeclarations);
                if (!addRelatedName(foreign.type, namespaces(foreignType)))
                    unresolvedForeignNames.insert(foreign.type);
            }
        }
    }

    const auto addReference
            = [&](const MetaType &type, QSet<QAnyStringView> *processedRelatedNames,
                  FoundType::Origin origin, TypeRelation relation,
                  ContextOpacity contextOpacity = ContextOpacity::FullyRegistered) {
        if (type.isEmpty())
            return;
        QAnyStringView qualifiedName = type.qualifiedClassName();
        m_referencedTypes.append(qualifiedName);
        const qsizetype size = processedRelatedNames->size();
        processedRelatedNames->insert(qualifiedName);

        if (processedRelatedNames->size() == size) {
            // a type might change from opaque to non-opaque

            // Remove from opaque types - might happen if the type is used as both a property
            // and e.g. as a base type
            switch (relation) {
            case TypeRelation::Property:
            case TypeRelation::Return:
            case TypeRelation::Argument:
                // still only opaque, we can return
                return;
            case TypeRelation::Base:
                // A base type only stays opaque if the type it is a base of is opaque, too.
                // Otherwise it has to be properly registered and we fall through to the regular
                // handling below.
                if (contextOpacity == ContextOpacity::Opaque)
                    return;
                break;
            default:
                break;
            }
            if (removeFromSortedMetaTypeList(m_opaqueTypes, type) == NotFound)
                return; // type used in two non-opaque usages; e.g. Base + Extension
        } else {
            typeQueue.enqueue(type);
        }

        if (origin == FoundType::OwnTypes)
            return;

        switch (relation) {
        case TypeRelation::Property:
        case TypeRelation::Return:
        case TypeRelation::Argument:
            insertIntoSortedMetaTypeList(m_opaqueTypes, type);
            return;
        case TypeRelation::Base:
            // Only the bases of opaque types are themselves opaque. The bases of regular types
            // are registered normally, just like any other related type.
            if (contextOpacity == ContextOpacity::Opaque) {
                insertIntoSortedMetaTypeList(m_opaqueTypes, type);
                return;
            }
            break;
        default:
            break;
        }

        // Add to own types since we need it for our registrations.
        insertIntoSortedMetaTypeList(m_types, type);

        // We only add types to m_types of which we know we can reach them via the existing
        // m_includes. We do not add to m_includes, because any further headers may not be
        // #include'able.

        // Remove from the foreign types to avoid the ODR warning.
        removeFromSortedMetaTypeList(m_foreignTypes, type);
    };

    const auto addInterface
            = [&](QAnyStringView typeName, const QList<QAnyStringView> &namespaces,
                  TypeRelation relation, ContextOpacity contextOpacity = ContextOpacity::FullyRegistered) {
        if (const FoundType other = QmlTypesClassDescription::findType(
                    m_types, m_foreignTypes, typeName, namespaces)) {
            if (!other.native.isEmpty()) {
                addReference(
                        other.native, &processedRelatedNativeNames, other.nativeOrigin, relation,
                        contextOpacity);
                return true;
            }
        } else {
            // Do not warn about unresolved interfaces.
            // They don't have to have Q_OBJECT or Q_GADGET.
            unresolvedForeignNames.insert(typeName);
        }

        processedRelatedNativeNames.insert(typeName);
        return false;
    };

    const auto doAddReferences = [&](QAnyStringView typeName,
                                     const QList<QAnyStringView> &namespaces,
                                     TypeRelation relation,
                                     ContextOpacity contextOpacity = ContextOpacity::FullyRegistered) {
        if (const FoundType other = QmlTypesClassDescription::findType(
                    m_types, m_foreignTypes, typeName, namespaces)) {
            addReference(
                    other.native, &processedRelatedNativeNames, other.nativeOrigin, relation,
                    contextOpacity);
            addReference(
                    other.javaScript, &processedRelatedJavaScriptNames, other.javaScriptOrigin,
                    relation, contextOpacity);
            return true;
        }

        return false;
    };

    const auto addType = [&](const MetaType &context, QAnyStringView typeName,
                             const QList<QAnyStringView> &namespaces, TypeRelation relation) {
        if (doAddReferences(typeName, namespaces, relation))
            return true;

        // If it's an enum, add the surrounding type.
        const QLatin1StringView separator("::");
        if (const qsizetype index = lastIndexOf(typeName, separator); index > 0) {
            if (const FoundType other = QmlTypesClassDescription::findType(
                        m_types, m_foreignTypes, typeName.left(index), namespaces)) {

                const QAnyStringView enumName = typeName.mid(index + separator.length());

                for (const Enum &enumerator : other.native.enums()) {
                    if (enumerator.name != enumName && enumerator.alias != enumName)
                        continue;

                    addReference(
                            other.native, &processedRelatedNativeNames, other.nativeOrigin,
                            relation);
                    addReference(
                            other.javaScript, &processedRelatedJavaScriptNames,
                            other.javaScriptOrigin, relation);
                    return true;
                }
            }
        }

        // If it's an enum of the context type itself, we don't have to do anything.
        for (const Enum &enumerator : context.enums()) {
            if (enumerator.name == typeName || enumerator.alias == typeName)
                return true;
        }

        // If we've detected this type as unresolved foreign and it actually belongs to this module,
        // we'll get to it again when we process it as foreign type. In that case we'll look at the
        // special cases for sequences and extensions.
        if (!unresolvedForeignNames.contains(typeName) && !isPrimitive(typeName)) {
            // Also, if the type is used as a property/method argument, we want to still make it
            // known to tooling as an opaque type, and don't warn about it
            switch (relation) {
            case TypeRelation::Property:
            case TypeRelation::Return:
            case TypeRelation::Argument: {
                MetaType type = MetaType::createOpaqueType(typeName);
                insertIntoSortedMetaTypeList(m_opaqueTypes, type);
                break;
            }
            default:
                warning(context) << typeName << "is used as" << typeRelationString(relation)
                                 << "type but cannot be found.";
            }
        }

        processedRelatedNativeNames.insert(typeName);
        processedRelatedJavaScriptNames.insert(typeName);
        return false;
    };



    const auto addSupers = [&](const MetaType &context, const QList<QAnyStringView> &namespaces,
                               ContextOpacity contextOpacity) {
        for (const Interface &iface : context.ifaces())
            addInterface(interfaceName(iface), namespaces, TypeRelation::Base, contextOpacity);

        // We don't warn about missing bases for value types. They don't have to be registered.
        bool warnAboutSupers = context.kind() != MetaType::Kind::Gadget;

        QList<QAnyStringView> missingSupers;

        for (const BaseType &superObject : context.superClasses()) {
            if (superObject.access != Access::Public)
                continue;

            QAnyStringView typeName = superObject.name;
            if (doAddReferences(typeName, namespaces, TypeRelation::Base, contextOpacity))
                warnAboutSupers = false;
            else
                missingSupers.append(typeName);
        }

        for (QAnyStringView typeName : std::as_const(missingSupers)) {
            // If we've found one valid base type, don't complain about the others.
            if (warnAboutSupers
                    && !unresolvedForeignNames.contains(typeName)
                    && !isPrimitive(typeName)) {
                warning(context) << typeName << "is used as base type but cannot be found.";
            }

            processedRelatedNativeNames.insert(typeName);
            processedRelatedJavaScriptNames.insert(typeName);
        }
    };

    const auto addProperties = [&](const MetaType &context,
                                   const QList<QAnyStringView> &namespaces) {
        for (const Property &property : context.properties()) {
            ResolvedTypeAlias resolved(property.type, m_usingDeclarations);
            if (!resolved.type.isEmpty())
                addType(context, resolved.type, namespaces, TypeRelation::Property);
        }
    };

    const auto addMethods = [&](const MetaType &context,
                                const QList<QAnyStringView> &namespaces) {
        for (const Method::Container &methods
             : {context.methods(), context.constructors(), context.sigs() }) {
            for (const Method &methodObject : methods) {
                if (methodObject.access != Access::Public)
                    continue;
                for (const Argument &argument : std::as_const(methodObject.arguments)) {
                    ResolvedTypeAlias resolved(argument.type, m_usingDeclarations);
                    if (!resolved.type.isEmpty())
                        addType(context, resolved.type, namespaces,  TypeRelation::Argument);
                }

                ResolvedTypeAlias resolved(methodObject.returnType, m_usingDeclarations);
                if (!resolved.type.isEmpty())
                    addType(context, resolved.type, namespaces,  TypeRelation::Return);
            }
        }
    };

    const auto addEnums = [&](const MetaType &context,
                              const QList<QAnyStringView> &namespaces) {
        for (const Enum &enumerator : context.enums()) {
            ResolvedTypeAlias resolved(enumerator.type, m_usingDeclarations);
            if (!resolved.type.isEmpty())
                addType(context, resolved.type, namespaces,  TypeRelation::Enum);
        }
    };

    const auto addRelation = [&](const MetaType &classDef, const ClassInfo &obj,
                                 const QList<QAnyStringView> &namespaces) {
        const QAnyStringView objNameValue = obj.name;
        if (objNameValue == S_ATTACHED) {
            addType(classDef, obj.value, namespaces,  TypeRelation::Attached);
            return true;
        } else if (objNameValue == S_SEQUENCE) {
            ResolvedTypeAlias value(obj.value, m_usingDeclarations);
            addType(classDef, value.type, namespaces,  TypeRelation::SequenceValue);
            return true;
        } else if (objNameValue == S_EXTENDED) {
            const QAnyStringView value = obj.value;
            addType(classDef, value, namespaces,  TypeRelation::Extension);
            return true;
        }
        return false;
    };

    // Then recursively iterate the super types and attached types, marking the
    // ones we are interested in as related.
    while (!typeQueue.isEmpty()) {
        QAnyStringView unresolvedForeign;

        const MetaType classDef = typeQueue.dequeue();
        const QList<QAnyStringView> namespaces = MetaTypesJsonProcessor::namespaces(classDef);
        const ContextOpacity classDefOpacity = sortedMetaTypeListContains(m_opaqueTypes, classDef)
                ? ContextOpacity::Opaque
                : ContextOpacity::FullyRegistered;

        for (const ClassInfo &obj : classDef.classInfos()) {
            if (addRelation(classDef, obj, namespaces))
                continue;
            if (obj.name != S_FOREIGN)
                continue;

            const QAnyStringView foreignClassName = obj.value;

            // A type declared as QML_FOREIGN will usually be a foreign type, but it can
            // actually be an additional registration of a local type, too.
            if (const FoundType found = QmlTypesClassDescription::findType(
                        m_foreignTypes, {}, foreignClassName, namespaces)) {
                const MetaType other = found.select(classDef, "Foreign");
                const QList<QAnyStringView> otherNamespaces
                        = MetaTypesJsonProcessor::namespaces(other);
                addSupers(other, otherNamespaces, classDefOpacity);
                addProperties(other, otherNamespaces);
                addMethods(other, otherNamespaces);
                addEnums(other, otherNamespaces);

                for (const ClassInfo &obj : other.classInfos()) {
                    if (addRelation(classDef, obj, otherNamespaces))
                        break;
                    // No, you cannot chain S_FOREIGN declarations. Sorry.
                }
            } else if (!QmlTypesClassDescription::findType(
                               m_types, {}, foreignClassName, namespaces)) {
                unresolvedForeign = foreignClassName;
            }
        }

        if (!unresolvedForeign.isEmpty() && !isPrimitive(unresolvedForeign)) {
            warning(classDef)
                    << unresolvedForeign
                    << "is declared as foreign type, but cannot be found.";
        }

        addSupers(classDef, namespaces, classDefOpacity);
        addProperties(classDef, namespaces);
        addMethods(classDef, namespaces);
        addEnums(classDef, namespaces);
    }
}

void MetaTypesJsonProcessor::sortTypes(QList<MetaType> &types)
{
    std::sort(types.begin(), types.end(), qualifiedClassNameLessThan);
}

QString MetaTypesJsonProcessor::resolvedInclude(QAnyStringView include)
{
    if (!m_privateIncludes)
        return include.toString();

    if (endsWith(include, "_p.h"_L1))
        return QLatin1String("private/") + include.toString();

    if (startsWith(include, "qplatform"_L1) ||  startsWith(include, "qwindowsystem"_L1))
        return QLatin1String("qpa/") + include.toString();

    return include.toString();
}

void MetaTypesJsonProcessor::processTypes(const QCborMap &types)
{
    const QString include = resolvedInclude(types[S_INPUT_FILE].toStringView());
    const QCborArray classes = types[S_CLASSES].toArray();
    for (const QCborValue &cls : classes) {
        const MetaType classDef(cls.toMap(), include);

        const PreProcessResult preprocessed = preProcess(classDef, PopulateMode::Yes);
        switch (preprocessed.mode) {
        case NamespaceRegistration:
        case GadgetRegistration:
        case ObjectRegistration: {
            if (!endsWith(include, QLatin1String(".h"))
                    && !endsWith(include, QLatin1String(".hpp"))
                    && !endsWith(include, QLatin1String(".hxx"))
                    && !endsWith(include, QLatin1String(".hh"))
                    && !endsWith(include, QLatin1String(".py"))
                    && contains(include, QLatin1Char('.'))) {
                warning(include)
                        << "Class" << classDef.qualifiedClassName()
                        << "is declared in" << include << "which appears not to be a header."
                        << "The compilation of its registration to QML may fail.";
            }
            m_includes.append(include);
            m_types.emplaceBack(classDef);
            break;
        }
        case NoRegistration:
            m_foreignTypes.emplaceBack(classDef);
            break;
        }

        if (!preprocessed.foreignPrimitive.isEmpty()) {
            m_primitiveTypes.emplaceBack(preprocessed.foreignPrimitive);
            m_primitiveTypes.append(preprocessed.primitiveAliases);
        }

        if (preprocessed.usingDeclaration.isValid())
            m_usingDeclarations.append(preprocessed.usingDeclaration);
    }
}

void MetaTypesJsonProcessor::processForeignTypes(const QCborMap &types)
{
    const QString include = resolvedInclude(types[S_INPUT_FILE].toStringView());
    const QCborArray classes = types[S_CLASSES].toArray();
    for (const QCborValue &cls : classes) {
        const MetaType classDef(cls.toMap(), include);
        PreProcessResult preprocessed = preProcess(classDef, PopulateMode::No);

        m_foreignTypes.emplaceBack(classDef);
        if (!preprocessed.foreignPrimitive.isEmpty()) {
            m_primitiveTypes.emplaceBack(preprocessed.foreignPrimitive);
            m_primitiveTypes.append(preprocessed.primitiveAliases);
        }

        if (preprocessed.usingDeclaration.isValid())
            m_usingDeclarations.append(preprocessed.usingDeclaration);
    }
}

static QTypeRevision getRevision(const QCborMap &cbor)
{
    const auto it = cbor.find(S_REVISION);
    return it == cbor.end()
            ? QTypeRevision()
            : QTypeRevision::fromEncodedVersion(it->toInteger());
}

static Access getAccess(const QCborMap &cbor)
{
    const QAnyStringView access = cbor[S_ACCESS].toStringView();
    if (access == S_PUBLIC)
        return Access::Public;
    if (access == S_PROTECTED)
        return Access::Protected;
    return Access::Private;
}

BaseType::BaseType(const QCborMap &cbor)
    : name(cbor[S_NAME].toStringView())
    , access(getAccess(cbor))
{
}

ClassInfo::ClassInfo(const QCborMap &cbor)
    : name(cbor[S_NAME].toStringView())
    , value(cbor[S_VALUE].toStringView())
{
}

Interface::Interface(const QCborValue &cbor)
{
    if (cbor.isArray()) {
        QCborArray needlessWrapping = cbor.toArray();
        className = needlessWrapping.size() > 0
                ? needlessWrapping[0].toMap()[S_CLASS_NAME].toStringView()
                : QAnyStringView();
    } else {
        className = cbor.toMap()[S_CLASS_NAME].toStringView();
    }
}

Property::Property(const QCborMap &cbor)
    : name(cbor[S_NAME].toStringView())
    , type(cbor[S_TYPE].toStringView())
    , member(cbor[S_MEMBER].toStringView())
    , read(cbor[S_READ].toStringView())
    , write(cbor[S_WRITE].toStringView())
    , reset(cbor[S_RESET].toStringView())
    , notify(cbor[S_NOTIFY].toStringView())
    , bindable(cbor[S_BINDABLE].toStringView())
    , privateClass(cbor[S_PRIVATE_CLASS].toStringView())
    , index(cbor[S_INDEX].toInteger(-1))
    , lineNumber(cbor[S_LINENUMBER].toInteger(0))
    , revision(getRevision(cbor))
    , isFinal(cbor[S_FINAL].toBool())
    , isVirtual(cbor[S_VIRTUAL].toBool())
    , isOverride(cbor[S_OVERRIDE].toBool())
    , isConstant(cbor[S_CONSTANT].toBool())
    , isRequired(cbor[S_REQUIRED].toBool())
{
}

Argument::Argument(const QCborMap &cbor)
    : name(cbor[S_NAME].toStringView())
    , type(cbor[S_TYPE].toStringView())
{
}

Method::Method(const QCborMap &cbor, bool isConstructor)
    : name(cbor[S_NAME].toStringView())
    , returnType(cbor[S_RETURN_TYPE].toStringView())
    , index(cbor[S_INDEX].toInteger(InvalidIndex))
    , lineNumber(cbor[S_LINENUMBER].toInteger(0))
    , revision(getRevision(cbor))
    , access(getAccess(cbor))
    , isCloned(cbor[S_IS_CLONED].toBool())
    , isJavaScriptFunction(cbor[S_IS_JAVASCRIPT_FUNCTION].toBool())
    , isConstructor(isConstructor || cbor[S_IS_CONSTRUCTOR].toBool())
    , isConst(cbor[S_IS_CONST].toBool())
{
    const QCborArray args = cbor[S_ARGUMENTS].toArray();
    for (const QCborValue &argument : args)
        arguments.emplace_back(argument.toMap());

    if (arguments.size() == 1) {
        const QAnyStringView type = arguments[0].type;
        if (type == "QQmlV4FunctionPtr"_L1 || type == "QQmlV4Function*"_L1) {
            isJavaScriptFunction = true;
            arguments.clear();
        }
    }
}

Enum::Enum(const QCborMap &cbor)
    : name(cbor[S_NAME].toStringView())
    , alias(cbor[S_ALIAS].toStringView())
    , type(cbor[S_TYPE].toStringView())
    , lineNumber(cbor[S_LINENUMBER].toInteger(0))
    , isFlag(cbor[S_IS_FLAG].toBool())
    , isClass(cbor[S_IS_CLASS].toBool())
{
    const QCborArray vals = cbor[S_VALUES].toArray();
    for (const QCborValue &value : vals)
        values.emplace_back(value.toStringView());
}

MetaTypePrivate::MetaTypePrivate(const QCborMap &cbor, const QString &inputFile)
    : cbor(cbor)
    , inputFile(inputFile)
{
    className = cbor[S_CLASS_NAME].toStringView();
    lineNumber = cbor[S_LINENUMBER].toInteger(0);
    qualifiedClassName = cbor[S_QUALIFIED_CLASS_NAME].toStringView();

    const QCborArray cborSuperClasses = cbor[S_SUPER_CLASSES].toArray();
    for (const QCborValue &superClass : cborSuperClasses)
        superClasses.emplace_back(superClass.toMap());

    const QCborArray cborClassInfos = cbor[S_CLASS_INFOS].toArray();
    for (const QCborValue &classInfo : cborClassInfos)
        classInfos.emplace_back(classInfo.toMap());

    const QCborArray cborIfaces = cbor[S_INTERFACES].toArray();
    for (const QCborValue &iface : cborIfaces)
        ifaces.emplace_back(iface);

    const QCborArray cborProperties = cbor[S_PROPERTIES].toArray();
    for (const QCborValue &property : cborProperties)
        properties.emplace_back(property.toMap());

    for (const QCborArray &cborMethods : { cbor[S_SLOTS].toArray(), cbor[S_METHODS].toArray() }) {
        for (const QCborValue &method : cborMethods)
            methods.emplace_back(method.toMap(), false);
    }

    const QCborArray cborSigs = cbor[S_SIGNALS].toArray();
    for (const QCborValue &sig : cborSigs)
        sigs.emplace_back(sig.toMap(), false);

    const QCborArray cborConstructors = cbor[S_CONSTRUCTORS].toArray();
    for (const QCborValue &constructor : cborConstructors)
        constructors.emplace_back(constructor.toMap(), true);

    const QCborArray cborEnums = cbor[S_ENUMS].toArray();
    for (const QCborValue &enumerator : cborEnums)
        enums.emplace_back(enumerator.toMap());

    if (cbor[S_GADGET].toBool())
        kind = Kind::Gadget;
    else if (cbor[S_OBJECT].toBool())
        kind = Kind::Object;
    else if (cbor[S_NAMESPACE].toBool())
        kind = Kind::Namespace;
}

MetaType::MetaType(const QCborMap &cbor, const QString &inputFile)
    : d(s_pool.emplace_back(std::make_unique<MetaTypePrivate>(cbor, inputFile)).get())
{}

/* metatype for a type about which we don't know anything
   Caller is expected to track duplicates, as we otherwise end up with two opaque types
   of the same name
 */
MetaType MetaType::createOpaqueType(QAnyStringView typeName)
{
    auto priv = s_pool.emplace_back(std::make_unique<MetaTypePrivate>()).get();
    priv->className = typeName;
    priv->qualifiedClassName = typeName;
    priv->kind = MetaType::Kind::Gadget;

    MetaType type;
    type.d = priv;
    return type;
}

QT_END_NAMESPACE
