// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
// Qt-Security score:critical reason:data-parser

#include "qqmltyperegistrarutils_p.h"
#include "qmetatypesjsonprocessor_p.h"

#include <QtCore/qcborarray.h>
#include <QtCore/qcbormap.h>
#include <QtCore/qcborvalue.h>
#include <QtCore/qdebug.h>
#include <QtCore/qfile.h>
#include <QtCore/qdiriterator.h>
#include <QtCore/private/qduplicatetracker_p.h>
#include <QtCore/qtextstream.h>

using namespace Qt::Literals::StringLiterals;

QT_BEGIN_NAMESPACE

QTypeRevision handleInMinorVersion(QTypeRevision revision, int majorVersion)
{
    if (!revision.hasMajorVersion() && revision.hasMinorVersion()) {
        // this version has been obtained by QML_{ADDED,REMOVED}_IN_MINOR_VERSION
        revision = QTypeRevision::fromVersion(majorVersion, revision.minorVersion());
    }
    return revision;
}

QAnyStringView interfaceName(const Interface &iface)
{
    return iface.className;
}

static QDebug message(QDebug base, QAnyStringView message, QAnyStringView fileName, int lineNumber)
{
    // Formatted such that it becomes a link in QtCreator (even with "::" ending)
    const QString lineString = lineNumber ? QString::number(lineNumber) : QString();
    return (base.noquote().nospace()
            << message << ": " << fileName << ":" << lineString << ":").space();
}

QDebug warning(QAnyStringView fileName, int lineNumber)
{
    return message(qWarning(), "Warning", fileName, lineNumber);
}

QDebug warning(const MetaType &classDef)
{
    const QAnyStringView file = classDef.inputFile();
    int lineNo = classDef.lineNumber();
    if (!file.isEmpty())
        return warning(file, lineNo);

    return warning(classDef.qualifiedClassName());
}

QDebug error(QAnyStringView fileName, int lineNumber)
{
    return message(qCritical(), "Error", fileName, lineNumber);
}

/*!
    \internal
    \a pathToList points to a file listing all qt.parts.conf files
    \a pathToFinalQtConfsList points to a file that lists the final merged qt.conf file location
       for each qt.parts.conf file
    In any given directory, there might be more than one qt.parts.conf file (especially on Winodws).
    We need to merge all import paths for a given folder (but want to avoid duplicate entries).

    The order of the import paths is significant: the QML engine resolves a module against its
    import paths in order and uses the first match. We therefore preserve the order in which the
    paths are listed within each qt.parts.conf file, so that the file's author decides which of
    two paths providing the same module wins. The order between separate qt.parts.conf files in
    the same directory is however that of QDirIterator, and so arbitrary.
 */
int mergeQtConfFiles(const QString &pathToList, const QString &pathToMergedQtConfsList)
{
    QFile listFile(pathToList);
    if (!listFile.open(QFile::ReadOnly | QFile::Text))
        return EXIT_FAILURE;
    QHash<QString, QStringList> directoryToNecessaryImports;
    while (!listFile.atEnd()) {
        QByteArray partFilePath = listFile.readLine().trimmed();
        QString directoryPath = QFileInfo(QString::fromUtf8(partFilePath)).absolutePath();
        QDirIterator dirIt(directoryPath, { "*_qt.part.conf"_L1 }, QDir::Filter::Files );
        while (dirIt.hasNext()) {
            QFile partialFile(dirIt.next());
            if (!partialFile.open(QFile::ReadOnly | QFile::Text)) {
                qCritical() << "could not open" << partialFile.fileName();
                return EXIT_FAILURE;
            }
            while (!partialFile.atEnd()) {
                QByteArray import = partialFile.readLine().trimmed();
                directoryToNecessaryImports[directoryPath].append(QString::fromUtf8(import));
            }
        }
    }

    // Collect the merged qt paths.
    QFile mergedPathsListFile(pathToMergedQtConfsList);
    if (!mergedPathsListFile.open(QFile::ReadOnly | QFile::Text)) {
        qCritical() << "could not open" << mergedPathsListFile.fileName();
        return EXIT_FAILURE;
    }
    QHash<QString, QString> partialPathToMergedPath;
    while (!mergedPathsListFile.atEnd()) {
        QByteArray row = mergedPathsListFile.readLine().trimmed();
        QList<QByteArray> parts = row.split(';');
        Q_ASSERT(parts.size() == 2);
        const QString partialFileDirectoryPath =
            QFileInfo(QString::fromUtf8(parts[0])).absolutePath();
        const QString mergedFilePath = QString::fromUtf8(parts[1]);
        partialPathToMergedPath.insert(partialFileDirectoryPath, mergedFilePath);
    }

    for (const QString &directoryPath: directoryToNecessaryImports.keys()) {
        if (!partialPathToMergedPath.contains(directoryPath)) {
            qCritical() << "no merged qt.conf file found for directory" << directoryPath;
            return EXIT_FAILURE;
        }
        const QString mergedQtConfFilePath = partialPathToMergedPath.value(directoryPath);

        // Pre-create the directory in case it doesn't exist yet. Especially relevant for the
        // 'Resources' subdirectory for macOS bundles.
        QDir().mkpath(QFileInfo(mergedQtConfFilePath).absolutePath());

        QFile consolidatedQtConfFile(mergedQtConfFilePath);
        if (!consolidatedQtConfFile.open(QFile::WriteOnly | QFile::Text)) {
            qCritical() << "could not open" << consolidatedQtConfFile.fileName();
            return EXIT_FAILURE;
        }
        QTextStream out(&consolidatedQtConfFile);
        QStringList allIncludes = directoryToNecessaryImports.value(directoryPath);
        allIncludes.removeDuplicates();
        out << "[Config]\nMergeQtConf = true\n"
            << "[Paths]\nQmlImports = "
            << allIncludes.join(u",") << Qt::endl;
    }
    QFile outfile(pathToList + u".done");
    if (!outfile.open(QFile::WriteOnly | QFile::Text))
        return EXIT_FAILURE;
    outfile.write(QByteArray::number(QDateTime::currentSecsSinceEpoch()));
    return EXIT_SUCCESS;
}

QT_END_NAMESPACE
