// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:trusted-sources

#include "qqmlcodemodel_p.h"
#include "qqmllsplugin_p.h"
#include "qtextdocument_p.h"
#include "qqmllsutils_p.h"

#include <QtCore/qfileinfo.h>
#include <QtCore/qdir.h>
#include <QtCore/qthreadpool.h>
#include <QtCore/qlibraryinfo.h>
#include <QtCore/qprocess.h>
#include <QtCore/qdiriterator.h>

#if QT_CONFIG(settings)
#  include <QtCore/qsettings.h>
#endif

#include <QtQmlDom/private/qqmldomtop_p.h>
#include <QtQmlCompiler/private/qqmljsutils_p.h>

#include <memory>
#include <algorithm>

QT_BEGIN_NAMESPACE

namespace QmlLsp {

Q_STATIC_LOGGING_CATEGORY(codeModelLog, "qt.languageserver.codemodel")

using namespace QQmlJS::Dom;
using namespace Qt::StringLiterals;

/*!
\internal
\class QQmlCodeModel

The code model offers a view of the current state of the current files, and traks open files.
All methods are threadsafe, and generally return immutable or threadsafe objects that can be
worked on from any thread (unless otherwise noted).
The idea is the let all other operations be as lock free as possible, concentrating all tricky
synchronization here.

\section2 Global views
\list
\li currentEnv() offers a view that contains the latest version of all the loaded files
\li validEnv() is just like current env but stores only the valid (meaning correctly parsed,
  not necessarily without errors) version of a file, it is normally a better choice to load the
  dependencies/symbol information from
\endlist

\section2 OpenFiles
\list
\li snapshotByUrl() returns an OpenDocumentSnapshot of an open document. From it you can get the
  document, its latest valid version, scope, all connected to a specific version of the document
  and immutable. The signal updatedSnapshot() is called every time a snapshot changes (also for
  every partial change: document change, validDocument change, scope change).
\li openDocumentByUrl() is a lower level and more intrusive access to OpenDocument objects. These
  contains the current snapshot, and shared pointer to a Utils::TextDocument. This is *always* the
  current version of the document, and has line by line support.
  Working on it is more delicate and intrusive, because you have to explicitly acquire its mutex()
  before *any* read or write/modification to it.
  It has a version nuber which is supposed to always change and increase.
  It is mainly used for highlighting/indenting, and is immediately updated when the user edits a
  document. Its use should be avoided if possible, preferring the snapshots.
\endlist

\section2 Parallelism/Theading
Most operations are not parallel and usually take place in the main thread (but are still thread
safe).
There is one task that is executed in parallel: OpenDocumentUpdate.
OpenDocumentUpdate keeps the snapshots of the open documents up to date.

There is always a tension between being responsive, using all threads available, and avoid to hog
too many resources. One can choose different parallelization strategies, we went with a flexiable
approach.
We have (private) functions that execute part of the work: openUpdateSome(). These
do all locking needed, get some work, do it without locks, and at the end update the state of the
code model. If there is more work, then they return true. Thus while (xxxSome()); works until there
is no work left.

The internal addOpenToUpdate() add more work to do.

openNeedUpdate() checks if there is work to do, and if yes ensure that a
worker thread (or more) that work on it exist.
*/

QQmlCodeModel::QQmlCodeModel(const QByteArray &rootUrl, QObject *parent,
                             QQmlToolingSharedSettings *settings)
    : QObject{ parent },
      m_rootUrl(rootUrl),
      m_currentEnv(std::make_shared<DomEnvironment>(
              QLibraryInfo::paths(QLibraryInfo::QmlImportsPath),
              DomEnvironment::Option::SingleThreaded, DomCreationOption::Extended)),
      m_validEnv(std::make_shared<DomEnvironment>(
              m_currentEnv.ownerAs<DomEnvironment>()->loadPaths(),
              DomEnvironment::Option::SingleThreaded, DomCreationOption::Extended)),
      m_settings(settings)
{
}

/*!
\internal
Enable and initialize the functionality that uses CMake, if CMake exists. Runs the build once.
*/
void QQmlCodeModel::tryEnableCMakeCalls(QProcessScheduler *scheduler)
{
    Q_ASSERT(scheduler);
    if (cmakeStatus() == DoesNotHaveCMake)
        return;

    if (m_settings) {
        const QString cmakeCalls = u"no-cmake-calls"_s;
        m_settings->search(url2Path(m_rootUrl), { QString(), verbose() });
        if (m_settings->isSet(cmakeCalls) && m_settings->value(cmakeCalls).toBool()) {
            disableCMakeCalls();
            return;
        }
    }

    QObject::connect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, scheduler,
                     [this, scheduler] { callCMakeBuild(scheduler); });
    setCMakeStatus(HasCMake);
    callCMakeBuild(scheduler);
}

/*!
\internal
Disable the functionality that uses CMake, and remove the already watched paths if there are some.
*/
void QQmlCodeModel::disableCMakeCalls()
{
    QMutexLocker guard(&m_mutex);
    m_cmakeStatus = DoesNotHaveCMake;
    if (const QStringList toRemove = m_cppFileWatcher.files(); !toRemove.isEmpty())
        m_cppFileWatcher.removePaths(toRemove);
    QObject::disconnect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, nullptr, nullptr);
}

void QQmlCodeModel::prepareForShutdown()
{
    while (true) {
        bool shouldWait;
        {
            QMutexLocker l(&m_mutex);
            m_openDocumentsToUpdate.clear();
            m_state = State::Stopping;
            shouldWait = m_nUpdateInProgress != 0;
        }
        if (!shouldWait)
            break;
        QThread::yieldCurrentThread();
    }
}

QQmlCodeModel::~QQmlCodeModel()
{
    QObject::disconnect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, nullptr, nullptr);
    prepareForShutdown();
}

OpenDocumentSnapshot QQmlCodeModel::snapshotByUrl(const QByteArray &url)
{
    return openDocumentByUrl(url).snapshot;
}

void QQmlCodeModel::removeDirectory(const QByteArray &url)
{
    const QString path = url2Path(url);
    if (auto validEnvPtr = m_validEnv.ownerAs<DomEnvironment>())
        validEnvPtr->removePath(path);
    if (auto currentEnvPtr = m_currentEnv.ownerAs<DomEnvironment>())
        currentEnvPtr->removePath(path);
}

QString QQmlCodeModel::url2Path(const QByteArray &url, UrlLookup options)
{
    QString res;
    {
        QMutexLocker l(&m_mutex);
        res = m_url2path.value(url);
    }
    if (!res.isEmpty() && options == UrlLookup::Caching)
        return res;
    QUrl qurl(QString::fromUtf8(url));
    QFileInfo f(qurl.toLocalFile());
    QString cPath = f.canonicalFilePath();
    if (cPath.isEmpty())
        cPath = f.filePath();
    {
        QMutexLocker l(&m_mutex);
        if (!res.isEmpty() && res != cPath)
            m_path2url.remove(res);
        m_url2path.insert(url, cPath);
        m_path2url.insert(cPath, url);
    }
    return cPath;
}

void QQmlCodeModel::newOpenFile(const QByteArray &url, int version, const QString &docText)
{
    {
        QMutexLocker l(&m_mutex);
        auto &openDoc = m_openDocuments[url];
        if (!openDoc.textDocument)
            openDoc.textDocument = std::make_shared<Utils::TextDocument>();
        QMutexLocker l2(openDoc.textDocument->mutex());
        openDoc.textDocument->setVersion(version);
        openDoc.textDocument->setPlainText(docText);
    }
    addOpenToUpdate(url, NormalUpdate);
    openNeedUpdate();
}

OpenDocument QQmlCodeModel::openDocumentByUrl(const QByteArray &url)
{
    QMutexLocker l(&m_mutex);
    return m_openDocuments.value(url);
}

bool QQmlCodeModel::isEmpty() const
{
    QMutexLocker l(&m_mutex);
    return m_openDocuments.isEmpty();
}

RegisteredSemanticTokens &QQmlCodeModel::registeredTokens()
{
    QMutexLocker l(&m_mutex);
    return m_tokens;
}

const RegisteredSemanticTokens &QQmlCodeModel::registeredTokens() const
{
    QMutexLocker l(&m_mutex);
    return m_tokens;
}

void QQmlCodeModel::openNeedUpdate()
{
    qCDebug(codeModelLog) << "openNeedUpdate";
    const int maxThreads = 1;
    {
        QMutexLocker l(&m_mutex);
        if (m_openDocumentsToUpdate.isEmpty() || m_nUpdateInProgress >= maxThreads
            || m_state == State::Stopping) {
            return;
        }
        if (++m_nUpdateInProgress == 1)
            openUpdateStart();
    }
    QThreadPool::globalInstance()->start([this]() {
        QScopedValueRollback thread(m_openUpdateThread, QThread::currentThread());
        while (openUpdateSome()) { }
        emit openUpdateThreadFinished();
    });
}

bool QQmlCodeModel::openUpdateSome()
{
    qCDebug(codeModelLog) << "openUpdateSome start";
    QByteArray toUpdate;
    UpdatePolicy policy;
    {
        QMutexLocker l(&m_mutex);
        Q_ASSERT(QThread::currentThread() == m_openUpdateThread);

        if (m_openDocumentsToUpdate.isEmpty()) {
            if (--m_nUpdateInProgress == 0)
                openUpdateEnd();
            return false;
        }
        const auto it = m_openDocumentsToUpdate.begin();
        toUpdate = it.key();
        policy = it.value();
        m_openDocumentsToUpdate.erase(it);
    }
    bool hasMore = false;
    {
        auto guard = qScopeGuard([this, &hasMore]() {
            QMutexLocker l(&m_mutex);
            if (m_openDocumentsToUpdate.isEmpty()) {
                if (--m_nUpdateInProgress == 0)
                    openUpdateEnd();
                hasMore = false;
            } else {
                hasMore = true;
            }
        });
        openUpdate(toUpdate, policy);
    }
    return hasMore;
}

void QQmlCodeModel::openUpdateStart()
{
    qCDebug(codeModelLog) << "openUpdateStart";
}

void QQmlCodeModel::openUpdateEnd()
{
    qCDebug(codeModelLog) << "openUpdateEnd";
}

QStringList QQmlCodeModel::buildPathsForOpenedFiles()
{
    QStringList result;

    std::vector<QByteArray> urls = { m_rootUrl };
    {
        QMutexLocker guard(&m_mutex);
        for (auto it = m_openDocuments.begin(), end = m_openDocuments.end(); it != end; ++it)
            urls.push_back(it.key());
    }

    for (const auto &url : urls)
        result.append(buildPathsForFileUrl(url));

    // remove duplicates
    std::sort(result.begin(), result.end());
    result.erase(std::unique(result.begin(), result.end()), result.end());
    return result;
}

static int cmakeJobsFromSettings(QQmlToolingSharedSettings *settings, const QString &rootPath,
                                 int defaultValue)
{
    if (!settings)
        return defaultValue;
    const auto result = settings->search(rootPath);
    if (!result.isValid())
        return defaultValue;

    bool ok = false;
    const QString valueString = settings->value("CMakeJobs"_L1).toString();
    if (valueString == QQmlCodeModel::s_maxCMakeJobs)
        return QThread::idealThreadCount();

    const int cmakeJobs = settings->value("CMakeJobs"_L1).toInt(&ok);
    if (!ok || cmakeJobs < 1)
        return defaultValue;
    return cmakeJobs;
}

void QQmlCodeModel::callCMakeBuild(QProcessScheduler *scheduler)
{
    const QStringList buildPaths = buildPathsForOpenedFiles();
    const int cmakeJobs = cmakeJobsFromSettings(m_settings, url2Path(m_rootUrl), m_cmakeJobs);

    QList<QProcessScheduler::Command> commands;
    for (const auto &path : buildPaths) {
        if (!QFileInfo::exists(path + u"/.cmake"_s))
            continue;

        auto [program, arguments] = QQmlLSUtils::cmakeBuildCommand(path, cmakeJobs);
        commands.append({ std::move(program), std::move(arguments) });
    }
    if (commands.isEmpty())
        return;
    qCInfo(codeModelLog) << "Starting background build on paths" << buildPaths.join(", "_L1);
    scheduler->schedule(commands, m_rootUrl.isEmpty() ? "Default workspace"_ba : m_rootUrl);
}

void QQmlCodeModel::reloadAllOpenFiles()
{
    QMutexLocker guard(&m_mutex);
    for (auto it = m_openDocuments.begin(), end = m_openDocuments.end(); it != end; ++it)
        m_openDocumentsToUpdate[it.key()] = ForceUpdate;
    guard.unlock();
    openNeedUpdate();
}

/*!
\internal
Iterate the entire source directory to find all C++ files that have their names in fileNames, and
return all the found file paths.

This is an overapproximation and might find unrelated files with the same name.
*/
QStringList QQmlCodeModel::findFilePathsFromFileNames(const QStringList &_fileNamesToSearch,
                                                      const QSet<QString> &ignoredFilePaths)
{
    Q_ASSERT(!m_rootUrl.isEmpty());
    QStringList fileNamesToSearch{ _fileNamesToSearch };

    {
        QMutexLocker guard(&m_mutex);
        // ignore files that were not found last time
        fileNamesToSearch.erase(std::remove_if(fileNamesToSearch.begin(), fileNamesToSearch.end(),
                                               [this](const QString &fileName) {
                                                   return m_ignoreForWatching.contains(fileName);
                                               }),
                                fileNamesToSearch.end());
    }

    const QString rootDir = QUrl(QString::fromUtf8(m_rootUrl)).toLocalFile();
    qCDebug(codeModelLog) << "Searching for files to watch in workspace folder" << rootDir;

    const QStringList result =
            QQmlLSUtils::findFilePathsFromFileNames(rootDir, fileNamesToSearch, ignoredFilePaths);

    QMutexLocker guard(&m_mutex);
    for (const auto &fileName : fileNamesToSearch) {
        if (std::none_of(result.begin(), result.end(),
                         [&fileName](const QString &path) { return path.endsWith(fileName); })) {
            m_ignoreForWatching.insert(fileName);
        }
    }
    return result;
}

/*!
\internal
Find all C++ file names (not path, for file paths call \l findFilePathsFromFileNames on the result
of this method) that this qmlFile relies on.
*/
QStringList QQmlCodeModel::fileNamesToWatch(const DomItem &qmlFile)
{
    const QmlFile *file = qmlFile.as<QmlFile>();
    if (!file)
        return {};

    auto resolver = file->typeResolver();
    if (!resolver)
        return {};

    auto types = resolver->importedTypes();

    QStringList result;
    for (const auto &type : types) {
        if (!type.scope)
            continue;
        // note: the factory only loads composite types
        const bool isComposite = type.scope.factory() || type.scope->isComposite();
        if (isComposite)
            continue;

        const QString filePath = QFileInfo(type.scope->filePath()).fileName();
        if (!filePath.isEmpty())
            result << filePath;
    }

    std::sort(result.begin(), result.end());
    result.erase(std::unique(result.begin(), result.end()), result.end());

    return result;
}

/*!
\internal
Add watches for all C++ files that this qmlFile relies on, so a rebuild can be triggered when they
are modified. Is a no op if this is the fallback codemodel with empty root url.
*/
void QQmlCodeModel::addFileWatches(const DomItem &qmlFile)
{
    if (m_rootUrl.isEmpty())
        return;
    const auto filesToWatch = fileNamesToWatch(qmlFile);

    // remove already watched files to avoid a warning later on
    const QStringList alreadyWatchedFiles = m_cppFileWatcher.files();
    const QSet<QString> alreadyWatchedFilesSet(alreadyWatchedFiles.begin(),
                                               alreadyWatchedFiles.end());
    QStringList filepathsToWatch = findFilePathsFromFileNames(filesToWatch, alreadyWatchedFilesSet);

    if (filepathsToWatch.isEmpty())
        return;

    QMutexLocker guard(&m_mutex);
    const auto unwatchedPaths = m_cppFileWatcher.addPaths(filepathsToWatch);
    if (!unwatchedPaths.isEmpty()) {
        qCDebug(codeModelLog) << "Cannot watch paths" << unwatchedPaths << "from requested"
                              << filepathsToWatch;
    }
}

enum VersionCheckResult {
    ClosedDocument,
    VersionLowerThanDocument,
    VersionLowerThanSnapshot,
    VersionOk,
};

enum VersionCheckResultForValidDocument {
    VersionLowerThanValidSnapshot,
    VersionOkForValidDocument,
};

VersionCheckResult checkVersion(const OpenDocument& doc, int version)
{
    if (!doc.textDocument)
        return ClosedDocument;

    {
        QMutexLocker guard2(doc.textDocument->mutex());
        if (doc.textDocument->version() && *doc.textDocument->version() > version)
            return VersionLowerThanDocument;
    }

    if (doc.snapshot.docVersion && *doc.snapshot.docVersion >= version)
        return VersionLowerThanSnapshot;

    return VersionOk;
}

static VersionCheckResultForValidDocument checkVersionForValidDocument(const OpenDocument &doc,
                                                                       int version)
{
    if (doc.snapshot.validDocVersion && *doc.snapshot.validDocVersion >= version)
        return VersionLowerThanValidSnapshot;

    return VersionOkForValidDocument;
}

static void updateItemInSnapshot(const DomItem &item, const DomItem &validItem,
                                 const QByteArray &url, OpenDocument *doc, int version,
                                 UpdatePolicy policy)
{
    switch (policy == ForceUpdate ? VersionOk : checkVersion(*doc, version)) {
    case ClosedDocument:
        qCWarning(lspServerLog) << "Ignoring update to closed document" << QString::fromUtf8(url);
        return;
    case VersionLowerThanDocument:
        qCWarning(lspServerLog) << "Version" << version << "of document" << QString::fromUtf8(url)
                                << "is not the latest anymore";
        return;
    case VersionLowerThanSnapshot:
        qCWarning(lspServerLog) << "Skipping update of current doc to obsolete version" << version
                                << "of document" << QString::fromUtf8(url);
        return;
    case VersionOk:
        doc->snapshot.docVersion = version;
        doc->snapshot.doc = item;
        break;
    }

    if (!item.field(Fields::isValid).value().toBool(false)) {
        qCWarning(lspServerLog) << "avoid update of validDoc to " << version << "of document"
                                << QString::fromUtf8(url) << "as it is invalid";
        return;
    }

    switch (policy == ForceUpdate ? VersionOkForValidDocument
                                  : checkVersionForValidDocument(*doc, version)) {
    case VersionLowerThanValidSnapshot:
        qCWarning(lspServerLog) << "Skipping update of valid doc to obsolete version" << version
                                << "of document" << QString::fromUtf8(url);
        return;
    case VersionOkForValidDocument:
        doc->snapshot.validDocVersion = version;
        doc->snapshot.validDoc = validItem;
        break;
    }
}

void QQmlCodeModel::newDocForOpenFile(const QByteArray &url, int version, const QString &docText,
                                      QmlLsp::UpdatePolicy policy)
{
    Q_ASSERT(QThread::currentThread() == m_openUpdateThread);
    qCDebug(codeModelLog) << "updating doc" << url << "to version" << version << "("
                          << docText.size() << "chars)";

    const QString fPath = url2Path(url, UrlLookup::ForceLookup);
    DomItem newCurrent = m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item();

    // if the documentation root path is not set through the commandline,
    // try to set it from the settings file (.qmlls.ini file)
    if (documentationRootPath().isEmpty() && m_settings) {
        // note: settings already searched current file in importPathsForFile() call above
        const QString docDir = QStringLiteral(u"docDir");
        if (m_settings->isSet(docDir))
            setDocumentationRootPath(m_settings->value(docDir).toString());
    }

    Path p;
    auto newCurrentPtr = newCurrent.ownerAs<DomEnvironment>();
    newCurrentPtr->setLoadPaths(importPathsForUrl(url));
    newCurrentPtr->setResourceFiles(m_resourceFiles);
    newCurrentPtr->loadFile(FileToLoad::fromMemory(newCurrentPtr, fPath, docText),
                            [&p, this](Path, const DomItem &, const DomItem &newValue) {
                                const DomItem file = newValue.fileObject();
                                p = file.canonicalPath();
                                // Force population of the file by accessing isValid field. We
                                // don't want to populate the file after adding the file to the
                                // snapshot in updateItemInSnapshot.
                                file.field(Fields::isValid);
                                if (cmakeStatus() == HasCMake)
                                    addFileWatches(file);
                            });
    newCurrentPtr->loadPendingDependencies();
    if (p) {
        newCurrent.commitToBase(m_validEnv.ownerAs<DomEnvironment>());
        QMutexLocker l(&m_mutex);
        updateItemInSnapshot(m_currentEnv.path(p), m_validEnv.path(p), url, &m_openDocuments[url],
                             version, policy);
    }
    if (codeModelLog().isDebugEnabled()) {
        qCDebug(codeModelLog) << "Finished update doc of " << url << "to version" << version;
        snapshotByUrl(url).dump(qDebug() << "postSnapshot");
    }
    // we should update the scope in the future thus call addOpen(url)
    emit updatedSnapshot(url, policy);
}

void QQmlCodeModel::closeOpenFile(const QByteArray &url)
{
    QMutexLocker l(&m_mutex);
    m_openDocuments.remove(url);
}

QByteArray QQmlCodeModel::rootUrl() const
{
    QMutexLocker l(&m_mutex);
    return m_rootUrl;
}

QStringList QQmlCodeModel::buildPaths()
{
    QMutexLocker l(&m_mutex);
    return m_buildPaths;
}

QStringList QQmlCodeModel::importPathsForUrl(const QByteArray &url)
{
    QStringList result = importPaths();

    // fallback for projects targeting Qt < 6.10, that don't have .qmlls.build.ini files
    if (result.isEmpty() || result == QLibraryInfo::paths(QLibraryInfo::QmlImportsPath))
        result << buildPathsForFileUrl(url);

    const QString importPathsKey = u"importPaths"_s;
    const QString fileName = url2Path(url);
    if (m_settings && m_settings->search(fileName, { QString(), verbose() }).isValid()
        && m_settings->isSet(importPathsKey)) {
        result.append(m_settings->valueAsAbsolutePathList(importPathsKey, fileName));
    }
    return result;
}

void QQmlCodeModel::setImportPaths(const QStringList &importPaths)
{
    QMutexLocker guard(&m_mutex);
    m_importPaths = importPaths;

    if (const auto &env = m_currentEnv.ownerAs<DomEnvironment>())
        env->setLoadPaths(importPaths);
    if (const auto &env = m_validEnv.ownerAs<DomEnvironment>())
        env->setLoadPaths(importPaths);
}

QStringList QQmlCodeModel::resourceFiles() const
{
    QMutexLocker guard(&m_mutex);
    return m_resourceFiles;
}

void QQmlCodeModel::setResourceFiles(const QStringList &resourceFiles)
{
    QMutexLocker guard(&m_mutex);
    m_resourceFiles = resourceFiles;

    if (const auto &env = m_currentEnv.ownerAs<DomEnvironment>())
        env->setResourceFiles(resourceFiles);
    if (const auto &env = m_validEnv.ownerAs<DomEnvironment>())
        env->setResourceFiles(resourceFiles);
}

QStringList QQmlCodeModel::importPaths() const
{
    QMutexLocker guard(&m_mutex);
    return m_importPaths;
}

static QStringList withDependentBuildDirectories(QStringList &&buildPaths)
{
    // add dependent build directories
    QStringList res;
    std::reverse(buildPaths.begin(), buildPaths.end());
    const int maxDeps = 4;
    while (!buildPaths.isEmpty()) {
        res += buildPaths.takeLast();
        const QString &bPath = res.constLast();
        const QString bPathExtended = bPath + u"/_deps";
        if (QFile::exists(bPathExtended) && bPath.count(u"/_deps/"_s) < maxDeps) {
            for (const auto &fileInfo :
                 QDirListing{ bPathExtended, QDirListing::IteratorFlag::DirsOnly }) {
                buildPaths.append(fileInfo.absoluteFilePath());
            }
        }
    }
    return res;
}

QStringList QQmlCodeModel::buildPathsForFileUrl(const QByteArray &url)
{
    if (QStringList result = buildPaths(); !result.isEmpty())
        return withDependentBuildDirectories(std::move(result));

    // fallback: look in the user settings (.qmlls.ini files in the source directory)
    if (!m_settings || !m_settings->search(url2Path(url), { QString(), verbose() }).isValid())
        return {};

    constexpr QLatin1String buildDir = "buildDir"_L1;
    if (!m_settings->isSet(buildDir))
        return {};

    const QString fileName = url2Path(url);
    return withDependentBuildDirectories(m_settings->valueAsAbsolutePathList(buildDir, fileName));
}

void QQmlCodeModel::setDocumentationRootPath(const QString &path)
{
    {
        QMutexLocker l(&m_mutex);
        if (m_documentationRootPath == path)
            return;
        m_documentationRootPath = path;
    }
    m_helpManager.setDocumentationRootPath(path);
}

void QQmlCodeModel::setBuildPaths(const QStringList &paths)
{
    QMutexLocker l(&m_mutex);
    m_buildPaths = paths;
}

void QQmlCodeModel::openUpdate(const QByteArray &url, UpdatePolicy policy)
{
    std::optional<int> rNow = 0;
    QString docText;

    {
        QMutexLocker l(&m_mutex);
        Q_ASSERT(QThread::currentThread() == m_openUpdateThread);
        OpenDocument &doc = m_openDocuments[url];
        std::shared_ptr<Utils::TextDocument> document = doc.textDocument;
        if (!document)
            return;
        {
            QMutexLocker l2(document->mutex());
            rNow = document->version();
        }
        if (!rNow
            || (policy != ForceUpdate && doc.snapshot.docVersion
                && *doc.snapshot.docVersion == *rNow)) {
            return;
        }

        {
            QMutexLocker l2(doc.textDocument->mutex());
            rNow = doc.textDocument->version();
            docText = doc.textDocument->toPlainText();
        }
    }
    newDocForOpenFile(url, *rNow, docText, policy);
}

void QQmlCodeModel::addOpenToUpdate(const QByteArray &url, UpdatePolicy policy)
{
    {
        QMutexLocker l(&m_mutex);
        m_openDocumentsToUpdate[url] = policy;
    }
    openNeedUpdate();
}

QDebug OpenDocumentSnapshot::dump(QDebug dbg)
{
    dbg.noquote().nospace() << "{";
    dbg << "  url:" << QString::fromUtf8(url) << "\n";
    dbg << "  docVersion:" << (docVersion ? QString::number(*docVersion) : u"*none*"_s) << "\n";
    dbg << u"  doc:"
        << (doc ? u"%1chars"_s.arg(doc.field(Fields::code).value().toString().size()) : u"*none*"_s)
        << "\n";
    dbg << "  validDocVersion:"
        << (validDocVersion ? QString::number(*validDocVersion) : u"*none*"_s) << "\n";
    dbg << u"  validDoc:"
        << (validDoc ? u"%1chars"_s.arg(validDoc.field(Fields::code).value().toString().size())
                     : u"*none*"_s)
        << "\n";
    dbg << "  scopeDependenciesLoadTime:" << scopeDependenciesLoadTime << "\n";
    dbg << "  scopeDependenciesChanged" << scopeDependenciesChanged << "\n";
    dbg << "}";
    return dbg;
}

static ModuleSetting *moduleSettingFor(const QString &sourceFolder, ModuleSettings *moduleSettings,
                                       UpdatePolicy policy)
{
    if (policy != ForceUpdate)
        return &moduleSettings->emplaceBack(ModuleSetting {sourceFolder, {}, {}});

    auto it = std::find_if(
            moduleSettings->begin(), moduleSettings->end(),
            [&sourceFolder](const ModuleSetting s) { return s.sourceFolder == sourceFolder; });
    if (it == moduleSettings->end())
        return &moduleSettings->emplaceBack(ModuleSetting {sourceFolder, {}, {}});
    return &*it;
}

static QStringList asStringList(const QVariant &variant)
{
    return variant.toString().split(QDir::listSeparator(), Qt::SkipEmptyParts);
}

// hotfix for projects targeting Qt 6.11, where the .ini array starts at 0 instead of 1
static void handleArrayStartingAt0(QSettings *settings, ModuleSettings *moduleSettings,
                                   UpdatePolicy policy)
{
    static constexpr QLatin1String sourcePathKey = "workspaces/0/sourcePath"_L1;
    if (!settings->contains(sourcePathKey))
        return;

    ModuleSetting *moduleSetting =
            moduleSettingFor(settings->value(sourcePathKey).toString(), moduleSettings, policy);
    moduleSetting->importPaths = asStringList(settings->value("workspaces/0/importPaths"_L1));
    moduleSetting->resourceFiles = asStringList(settings->value("workspaces/0/resourceFiles"_L1));
}

static void loadWorkspacesV2(QSettings *settings, ModuleSettings *moduleSettings,
                             UpdatePolicy policy)
{
    handleArrayStartingAt0(settings, moduleSettings, policy);

    const int entries = settings->beginReadArray("workspaces"_L1);
    for (int i = 0; i < entries; ++i) {
        settings->setArrayIndex(i);

        if (!settings->contains("sourcePath"_L1))
            continue;

        ModuleSetting *moduleSetting =
                moduleSettingFor(settings->value("sourcePath").toString(), moduleSettings, policy);
        moduleSetting->importPaths = asStringList(settings->value("importPaths"_L1));
        moduleSetting->resourceFiles = asStringList(settings->value("resourceFiles"_L1));
    }
    settings->endArray();
}

void QQmllsBuildInformation::loadSettingsFrom(const QStringList &buildPaths, UpdatePolicy policy)
{
#if QT_CONFIG(settings)
    for (const QString &path : buildPaths) {
        if (policy != ForceUpdate && m_seenSettings.contains(path))
            continue;
        m_seenSettings.insert(path);

        const QString iniPath = QString(path).append("/.qt/.qmlls.build.ini"_L1);
        if (!QFile::exists(iniPath))
            continue;

        QSettings settings(iniPath, QSettings::IniFormat);
        m_docDir = settings.value("docDir"_L1).toString();
        const qsizetype version = settings.value("version"_L1, "1"_L1).toString().toInt();
        switch (version) {
        case 2: {
            loadWorkspacesV2(&settings, &m_moduleSettings, policy);
            break;
        }
        case 1:
        default: {
            for (const QString &group : settings.childGroups()) {
                settings.beginGroup(group);

                ModuleSetting *moduleSetting = moduleSettingFor(
                        QString(group).replace("<SLASH>"_L1, "/"_L1), &m_moduleSettings, policy);
                moduleSetting->importPaths = asStringList(settings.value("importPaths"_L1));
                moduleSetting->resourceFiles = asStringList(settings.value("resourceFiles"_L1));
                settings.endGroup();
            }
            break;
        }
        }
    }
#else
    Q_UNUSED(buildPaths);
#endif
}

QStringList QQmllsBuildInformation::importPathsFor(const QString &filePath)
{
    return settingFor(filePath).importPaths;
}

QStringList QQmllsBuildInformation::resourceFilesFor(const QString &filePath)
{
    return settingFor(filePath).resourceFiles;
}

ModuleSetting QQmllsBuildInformation::settingFor(const QString &filePath)
{
    ModuleSetting result;
    qsizetype longestMatch = 0;
    for (const ModuleSetting &setting : m_moduleSettings) {
        const qsizetype matchLength = setting.sourceFolder.size();
        if (filePath.startsWith(setting.sourceFolder) && matchLength > longestMatch) {
            result = setting;
            longestMatch = matchLength;
        }
    }
    QQmlToolingSettings::resolveRelativeImportPaths(filePath, &result.importPaths);
    return result;
}

void QQmllsBuildInformation::addModuleSetting(const ModuleSetting &moduleSetting)
{
    m_moduleSettings.append(moduleSetting);
}

void QQmllsBuildInformation::writeQmllsBuildIniContent(const QString &file) const
{
#if QT_CONFIG(settings)
    QSettings settings(file, QSettings::IniFormat);
    settings.setValue("docDir"_L1, m_docDir);
    settings.setValue("version"_L1, "2"_L1);
    settings.beginWriteArray("workspaces"_L1, m_moduleSettings.size());
    for (int i = 0; i < m_moduleSettings.size(); ++i) {
        settings.setArrayIndex(i);
        settings.setValue("sourcePath"_L1, m_moduleSettings[i].sourceFolder);
        settings.setValue("importPaths"_L1,
                          m_moduleSettings[i].importPaths.join(QDir::listSeparator()));
        settings.setValue("resourceFiles"_L1,
                          m_moduleSettings[i].resourceFiles.join(QDir::listSeparator()));
    }
    settings.endArray();

#else
    Q_UNUSED(file);
#endif
}

QQmllsBuildInformation::QQmllsBuildInformation() { }

} // namespace QmlLsp

QT_END_NAMESPACE
