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

#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QTextStream>

QT_BEGIN_NAMESPACE

namespace QQmlJS {

using namespace Qt::StringLiterals;

std::unique_ptr<AotStats> QQmlJSAotCompilerStats::s_instance = std::make_unique<AotStats>();
QString QQmlJSAotCompilerStats::s_moduleId;
bool QQmlJSAotCompilerStats::s_recordAotStats = false;

bool QQmlJS::AotStatsEntry::operator<(const AotStatsEntry &other) const
{
    if (line == other.line)
        return column < other.column;
    return line < other.line;
}

void AotStats::insert(const AotStats &other)
{
    for (const auto &[moduleUri, moduleStats] : other.m_entries.asKeyValueRange()) {
        m_entries[moduleUri].insert(moduleStats);
    }
}

std::optional<QList<QString>> AotStats::readAllLines(const QString &path)
{
    QFile aotstatsListFile(path);
    if (!aotstatsListFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug().noquote() << u"Could not open \"%1\" for reading"_s.arg(aotstatsListFile.fileName());
        return std::nullopt;
    }

    QStringList aotstatsFiles;
    QTextStream stream(&aotstatsListFile);
    while (!stream.atEnd())
        aotstatsFiles.append(stream.readLine());

    return aotstatsFiles;
}

std::optional<AotStats> AotStats::parseAotstatsFile(const QString &aotstatsPath)
{
    QFile file(aotstatsPath);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug().noquote() << u"Could not open \"%1\""_s.arg(aotstatsPath);
        return std::nullopt;
    }

    return AotStats::fromJsonDocument(QJsonDocument::fromJson(file.readAll()));
}

std::optional<AotStats> AotStats::aggregateAotstatsList(const QString &aotstatsListPath)
{
    const auto aotstatsFiles = readAllLines(aotstatsListPath);
    if (!aotstatsFiles.has_value())
        return std::nullopt;

    AotStats aggregated;
    if (aotstatsFiles->empty())
        return aggregated;

    for (const auto &aotstatsFile : aotstatsFiles.value()) {
        auto parsed = parseAotstatsFile(aotstatsFile);
        if (!parsed.has_value())
            return std::nullopt;
        aggregated.insert(parsed.value());
    }

    return aggregated;
}

static constexpr int S_AOTSTATS_FORMAT_REVISION = 1; // Added support for skipping

static constexpr QLatin1StringView S_CODEGEN_RESULT{ "codegenResult" };
static constexpr QLatin1StringView S_COLUMN{ "column" };
static constexpr QLatin1StringView S_DURATION_MICROSECONDS{ "durationMicroseconds" };
static constexpr QLatin1StringView S_ENTRIES{ "entries" };
static constexpr QLatin1StringView S_FILE_PATH{ "filePath" };
static constexpr QLatin1StringView S_FORMAT_REVISION{ "formatRevision" };
static constexpr QLatin1StringView S_FUNCTION_NAME{ "functionName" };
static constexpr QLatin1StringView S_LINE{ "line" };
static constexpr QLatin1StringView S_MESSAGE{ "message" };
static constexpr QLatin1StringView S_MODULES{ "modules" };
static constexpr QLatin1StringView S_MODULE_FILES{ "moduleFiles" };
static constexpr QLatin1StringView S_MODULE_ID{ "moduleId" };

std::optional<AotStats> AotStats::fromJsonDocument(const QJsonDocument &document)
{
    QJsonObject root = document.object();
    const QJsonValue revision = root[S_FORMAT_REVISION];
    if (revision.isUndefined() || revision.toInt() != S_AOTSTATS_FORMAT_REVISION) {
        qDebug() << "AotStats format revision missmatch. Please try again with a clean build.";
        return std::nullopt;
    }

    const QJsonArray modulesArray = root[S_MODULES].toArray();
    QQmlJS::AotStats result;
    for (const auto &modulesArrayEntry : std::as_const(modulesArray)) {
        const auto &moduleObject = modulesArrayEntry.toObject();
        QString moduleId = moduleObject[S_MODULE_ID].toString();
        const QJsonArray &filesArray = moduleObject[S_MODULE_FILES].toArray();

        QHash<QString, QList<AotStatsEntry>> files;
        for (const auto &filesArrayEntry : filesArray) {
            const QJsonObject &fileObject = filesArrayEntry.toObject();
            QString filepath = fileObject[S_FILE_PATH].toString();
            const QJsonArray &statsArray = fileObject[S_ENTRIES].toArray();

            QList<AotStatsEntry> stats;
            for (const auto &statsArrayEntry : statsArray) {
                const auto &statsObject = statsArrayEntry.toObject();
                QQmlJS::AotStatsEntry stat;
                auto micros = statsObject[S_DURATION_MICROSECONDS].toInteger();
                stat.codegenDuration = std::chrono::microseconds(micros);
                stat.functionName = statsObject[S_FUNCTION_NAME].toString();
                stat.message = statsObject[S_MESSAGE].toString();
                stat.line = statsObject[S_LINE].toInt();
                stat.column = statsObject[S_COLUMN].toInt();
                stat.codegenResult = QQmlJS::CodegenResult(statsObject[S_CODEGEN_RESULT].toInt());
                stats.append(std::move(stat));
            }

            std::sort(stats.begin(), stats.end());
            files[filepath] = std::move(stats);
        }

        result.m_entries[moduleId] = std::move(files);
    }

    return result;
}

QJsonDocument AotStats::toJsonDocument() const
{
    QJsonArray modulesArray;
    for (auto it1 = m_entries.begin(); it1 != m_entries.end(); ++it1) {
        const QString moduleId = it1.key();
        const QHash<QString, QList<AotStatsEntry>> &files = it1.value();

        QJsonArray filesArray;
        for (auto it2 = files.begin(); it2 != files.end(); ++it2) {
            const QString &filename = it2.key();
            const QList<AotStatsEntry> &stats = it2.value();

            QJsonArray statsArray;
            for (const auto &stat : stats) {
                QJsonObject statObject;
                auto micros = static_cast<qint64>(stat.codegenDuration.count());
                statObject.insert(S_DURATION_MICROSECONDS, micros);
                statObject.insert(S_FUNCTION_NAME, stat.functionName);
                statObject.insert(S_MESSAGE, stat.message);
                statObject.insert(S_LINE, stat.line);
                statObject.insert(S_COLUMN, stat.column);
                using CodegenResType = std::underlying_type_t<QQmlJS::CodegenResult>;
                statObject.insert(S_CODEGEN_RESULT,
                                  static_cast<CodegenResType>(stat.codegenResult));
                statsArray.append(statObject);
            }

            QJsonObject o;
            o.insert(S_FILE_PATH, filename);
            o.insert(S_ENTRIES, statsArray);
            filesArray.append(o);
        }

        QJsonObject o;
        o.insert(S_MODULE_ID, moduleId);
        o.insert(S_MODULE_FILES, filesArray);
        modulesArray.append(o);
    }

    QJsonObject root;
    root.insert(S_FORMAT_REVISION, S_AOTSTATS_FORMAT_REVISION);
    root.insert(S_MODULES, modulesArray);
    return QJsonDocument(root);
}

void AotStats::registerFile(const QString &moduleId, const QString &filepath)
{
    m_entries[moduleId][filepath] = {};
}

void AotStats::addEntry(
        const QString &moduleId, const QString &filepath, const AotStatsEntry &entry)
{
    m_entries[moduleId][filepath].append(entry);
}

bool AotStats::saveToDisk(const QString &filepath) const
{
    QFile file(filepath);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
        qDebug().noquote() << u"Could not open \"%1\""_s.arg(filepath);
        return false;
    }

    file.write(this->toJsonDocument().toJson(QJsonDocument::Indented));
    return true;
}

void QQmlJSAotCompilerStats::registerFile(const QString &filepath)
{
    QQmlJSAotCompilerStats::instance()->registerFile(s_moduleId, filepath);
}

void QQmlJSAotCompilerStats::addEntry(const QString &filepath, const QQmlJS::AotStatsEntry &entry)
{
    QQmlJSAotCompilerStats::instance()->addEntry(s_moduleId, filepath, entry);
}

} // namespace QQmlJS

QT_END_NAMESPACE
