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

#pragma once

#include "qmljs_global.h"
#include "qmljsbundle.h"
#include "qmljsdialect.h"
#include "qmljsdocument.h"
#include "qmljsinterpreter.h"

#include <cplusplus/CppDocument.h>
#include <utils/environment.h>
#include <utils/filepath.h>
#include <utils/futuresynchronizer.h>
#include <utils/id.h>
#include <utils/qrcparser.h>
#include <utils/synchronizedvalue.h>

#include <QFuture>
#include <QHash>
#include <QObject>
#include <QPointer>
#include <QStringList>
#include <QThreadPool>
#include <QVersionNumber>

QT_FORWARD_DECLARE_CLASS(QTimer)

namespace QmlJS {

class Snapshot;
class PluginDumper;

class QMLJS_EXPORT ModelManagerInterface : public QObject
{
    Q_OBJECT
    Q_DISABLE_COPY(ModelManagerInterface)

public:
    ModelManagerInterface(ModelManagerInterface &&) = delete;
    ModelManagerInterface &operator=(ModelManagerInterface &&) = delete;

    enum QrcResourceSelector {
        ActiveQrcResources,
        AllQrcResources
    };

    using ProjectBase = QObject; // This is a ProjectExplorer::Project in the using code.

    struct ProjectInfo
    {
        QPointer<ProjectBase> project;
        Utils::FilePaths sourceFiles;
        PathsAndLanguages importPaths;
        Utils::FilePaths activeResourceFiles;
        Utils::FilePaths allResourceFiles;
        Utils::FilePaths generatedQrcFiles;
        QHash<Utils::FilePath, QString> resourceFileContents;
        Utils::FilePaths applicationDirectories;
        QHash<QString, QString> moduleMappings; // E.g.: QtQuick.Controls -> MyProject.MyControls

        // whether trying to run qmldump makes sense
        bool tryQmlDump = false;
        bool qmlDumpHasRelocatableFlag = true;
        Utils::FilePath qmlDumpPath;
        Utils::Environment qmlDumpEnvironment;

        Utils::FilePath qtQmlPath;
        Utils::FilePath qmllsPath;
        QString qtVersionString;
        QmlJS::QmlLanguageBundles activeBundle;
        QmlJS::QmlLanguageBundles extendedBundle;
    };

    class WorkingCopy
    {
    public:
        using Table = QHash<Utils::FilePath, QPair<QString, int>>;

        void insert(const Utils::FilePath &fileName, const QString &source, int revision = 0)
        { m_elements.insert(fileName, {source, revision}); }

        bool contains(const Utils::FilePath &fileName) const
        { return m_elements.contains(fileName); }

        QString source(const Utils::FilePath &fileName) const
        { return m_elements.value(fileName).first; }

        QPair<QString, int> get(const Utils::FilePath &fileName) const
        { return m_elements.value(fileName); }

        Table all() const
        { return m_elements; }

    private:
        Table m_elements;
    };

    struct CppData
    {
        QList<LanguageUtils::FakeMetaObject::ConstPtr> exportedTypes;
        QHash<QString, QString> contextProperties;
    };

    using CppDataHash = QHash<QString, CppData>;

public:
    ModelManagerInterface(QObject *parent = nullptr);
    ~ModelManagerInterface() override;

    static Dialect guessLanguageOfFile(const Utils::FilePath &fileName);
    static QStringList globPatternsForLanguages(const QList<Dialect> &languages);
    static ModelManagerInterface *instance();
    static ModelManagerInterface *instanceForFuture(const QFuture<void> &future);
    static void writeWarning(const QString &msg);
    static WorkingCopy workingCopy();
    static Utils::FilePath qmllsForBinPath(const Utils::FilePath &binPath, const QVersionNumber &v);
    static Utils::FilePath qmlformatForBinPath(const Utils::FilePath &binPath, const QVersionNumber &v);

    QmlJS::Snapshot snapshot() const;
    QmlJS::Snapshot newestSnapshot() const;
    QThreadPool *threadPool();

    QSet<Utils::FilePath> scannedPaths() const;
    void removeFromScannedPaths(const PathsAndLanguages &pathsAndLanguages);

    void activateScan();
    void updateSourceFiles(const Utils::FilePaths &files, bool emitDocumentOnDiskChanged);
    void fileChangedOnDisk(const Utils::FilePath &path);
    void removeFiles(const Utils::FilePaths &files);
    QStringList qrcPathsForFile(const Utils::FilePath &file,
                                const QLocale *locale = nullptr,
                                ProjectBase *project = nullptr,
                                QrcResourceSelector resources = AllQrcResources);
    Utils::FilePaths filesAtQrcPath(
        const QString &path,
        const QLocale *locale = nullptr,
        ProjectBase *project = nullptr,
        QrcResourceSelector resources = AllQrcResources);
    QMap<QString, Utils::FilePaths> filesInQrcPath(
        const QString &path,
        const QLocale *locale = nullptr,
        ProjectBase *project = nullptr,
        bool addDirs = false,
        QrcResourceSelector resources = AllQrcResources);
    Utils::FilePath fileToSource(const Utils::FilePath &file);

    QList<ProjectInfo> projectInfos() const;
    bool containsProject(ProjectBase *project) const;
    ProjectInfo projectInfo(ProjectBase *project) const;

    void updateDocument(const QmlJS::Document::Ptr& doc);
    void updateLibraryInfo(const Utils::FilePath &path, const QmlJS::LibraryInfo &info);
    void emitDocumentChangedOnDisk(QmlJS::Document::Ptr doc);
    void updateQrcFile(const Utils::FilePath &path);
    ProjectInfo projectInfoForPath(const Utils::FilePath &path) const;
    QList<ProjectInfo> allProjectInfosForPath(const Utils::FilePath &path) const;

    Utils::FilePaths importPathsNames() const;
    QmlJS::QmlLanguageBundles activeBundles() const;
    QmlJS::QmlLanguageBundles extendedBundles() const;

    void loadPluginTypes(const Utils::FilePath &libraryPath,
                         const Utils::FilePath &importPath,
                         const QString &importUri,
                         const QString &importVersion);

    CppDataHash cppData() const;
    LibraryInfo builtins(const Document::Ptr &doc) const;
    ViewerContext completeVContext(const ViewerContext &vCtx,
                                   const Document::Ptr &doc = Document::Ptr(nullptr)) const;
    ViewerContext defaultVContext(Dialect language = Dialect::Qml,
                                  const Document::Ptr &doc = Document::Ptr(nullptr),
                                  bool autoComplete = true) const;
    ViewerContext projectVContext(Dialect language, const Document::Ptr &doc) const;

    void setDefaultVContext(const ViewerContext &vContext);
    ProjectInfo defaultProjectInfo() const;

    // Blocks until all parsing threads are done. Use for testing only!
    void test_joinAllThreads();

    template <typename T>
    void addFuture(const QFuture<T> &future) { addFuture(QFuture<void>(future)); }
    void addFuture(const QFuture<void> &future);

    QmlJS::Document::Ptr ensuredGetDocumentForPath(const Utils::FilePath &filePath);
    static void importScan(const WorkingCopy &workingCopy, const PathsAndLanguages &paths,
                           ModelManagerInterface *modelManager, bool emitDocChanged,
                           bool libOnly = true, bool forceRescan = false);
    static void importScanAsync(QPromise<void> &promise, const WorkingCopy& workingCopyInternal,
                                const PathsAndLanguages& paths, ModelManagerInterface *modelManager,
                                bool emitDocChanged, bool libOnly = true, bool forceRescan = false);

    virtual void resetCodeModel();
    void removeProjectInfo(ProjectBase *project);
    void maybeQueueCppQmlTypeUpdate(const CPlusPlus::Document::Ptr &doc);

    QFuture<void> refreshSourceFiles(const Utils::FilePaths &sourceFiles,
                                     bool emitDocumentOnDiskChanged);
    void waitForFinished();

signals:
    void documentUpdated(QmlJS::Document::Ptr doc);
    void documentChangedOnDisk(QmlJS::Document::Ptr doc);
    void aboutToRemoveFiles(const Utils::FilePaths &files);
    void libraryInfoUpdated(const Utils::FilePath &path, const QmlJS::LibraryInfo &info);
    void projectInfoUpdated(const ProjectInfo &pinfo);
    void projectPathChanged(const Utils::FilePath &projectPath);

protected:
    void updateProjectInfo(const ProjectInfo &pinfo, ProjectBase *p);

    Q_INVOKABLE void queueCppQmlTypeUpdate(const CPlusPlus::Document::Ptr &doc, bool scan);
    Q_INVOKABLE void asyncReset();
    virtual void startCppQmlTypeUpdate();
    QMutex *mutex() const;
    virtual QHash<QString,Dialect> languageForSuffix() const;
    virtual void writeMessageInternal(const QString &msg) const;
    virtual WorkingCopy workingCopyInternal() const;
    virtual void addTaskInternal(const QFuture<void> &result, const QString &msg,
                                 const Utils::Id taskId) const;

    static void parseLoop(QSet<Utils::FilePath> &scannedPaths,
                          QSet<Utils::FilePath> &newLibraries,
                          const WorkingCopy &workingCopyInternal,
                          Utils::FilePaths files,
                          ModelManagerInterface *modelManager,
                          QmlJS::Dialect mainLanguage,
                          bool emitDocChangedOnDisk,
                          const std::function<bool(qreal)> &reportProgress);
    static void parse(QPromise<void> &promise,
                      const WorkingCopy &workingCopyInternal,
                      Utils::FilePaths files,
                      ModelManagerInterface *modelManager,
                      QmlJS::Dialect mainLanguage,
                      bool emitDocChangedOnDisk);
    static void updateCppQmlTypes(QPromise<void> &promise, ModelManagerInterface *qmlModelManager,
                const CPlusPlus::Snapshot &snapshot,
                const QHash<QString, QPair<CPlusPlus::Document::Ptr, bool>> &documents);

    void maybeScan(const PathsAndLanguages &importPaths);
    void updateImportPaths();
    void loadQmlTypeDescriptionsInternal(
        const QString &path,
        QmlJS::CppQmlTypesLoader::BuiltinObjects &defaultQtObjects,
        QmlJS::CppQmlTypesLoader::BuiltinObjects &defaultLibraryObjects);
    void setDefaultProject(const ProjectInfo &pInfo, ProjectBase *p);
    void cancelAllThreads();

private:
    void joinAllThreads(bool cancelOnWait = false);
    void iterateQrcFiles(ProjectBase *project,
                         QrcResourceSelector resources,
                         const std::function<void(Utils::QrcParser::ConstPtr)> &callback);
    ViewerContext getVContext(const ViewerContext &vCtx, const Document::Ptr &doc, bool limitToProject) const;

    struct SyncedData
    {
        SyncedData(const Utils::FilePaths &defaultImportPaths);
        QmlJS::Snapshot m_validSnapshot;
        QmlJS::Snapshot m_newestSnapshot;
        PathsAndLanguages m_allImportPaths;
        Utils::FilePaths m_applicationPaths;
        Utils::FilePaths m_defaultImportPaths;
        QmlJS::QmlLanguageBundles m_activeBundles;
        QmlJS::QmlLanguageBundles m_extendedBundles;
        QHash<Dialect, QmlJS::ViewerContext> m_defaultVContexts;
        bool m_shouldScanImports = false;
        QSet<Utils::FilePath> m_scannedPaths;
        ProjectBase *m_defaultProject = nullptr;
        ProjectInfo m_defaultProjectInfo;
        QHash<ProjectBase *, ProjectInfo> m_projects;
        QMultiHash<Utils::FilePath, ProjectBase *> m_fileToProject;
    };
    Utils::SynchronizedValue<SyncedData> m_syncedData;

    QTimer *m_updateCppQmlTypesTimer = nullptr;
    QTimer *m_asyncResetTimer = nullptr;
    QHash<QString, QPair<CPlusPlus::Document::Ptr, bool>> m_queuedCppDocuments;
    QFuture<void> m_cppQmlTypesUpdater;
    Utils::QrcCache m_qrcCache;
    QHash<Utils::FilePath, QString> m_qrcContents;

    struct SyncedCppData
    {
        CppDataHash m_cppDataHash;
        QHash<QString, QList<CPlusPlus::Document::Ptr>> m_cppDeclarationFiles;
    };
    Utils::SynchronizedValue<SyncedCppData> m_syncedCppData;

    PluginDumper *m_pluginDumper = nullptr;

    mutable QMutex m_futuresMutex;
    Utils::FutureSynchronizer m_futureSynchronizer;

    bool m_indexerDisabled = false;
    QThreadPool m_threadPool;

private:
    Utils::FilePaths importPathsNames(const SyncedData &lockedData) const;

    static bool findNewQmlApplicationInPath(
        const Utils::FilePath &path,
        const Snapshot &snapshot,
        ModelManagerInterface *modelManager,
        QSet<Utils::FilePath> *newLibraries,
        Utils::SynchronizedValue<ModelManagerInterface::SyncedData>::unique_lock &lock);

    static void findNewLibraryImports(const Document::Ptr &doc,
                                      const Snapshot &snapshot,
                                      ModelManagerInterface *modelManager,
                                      Utils::FilePaths *importedFiles,
                                      QSet<Utils::FilePath> *scannedPaths,
                                      QSet<Utils::FilePath> *newLibraries,
                                      Utils::SynchronizedValue<SyncedData>::unique_lock *lock);

    static bool findNewQmlLibraryInPath(const Utils::FilePath &path,
                                        const Snapshot &snapshot,
                                        ModelManagerInterface *modelManager,
                                        Utils::FilePaths *importedFiles,
                                        QSet<Utils::FilePath> *scannedPaths,
                                        QSet<Utils::FilePath> *newLibraries,
                                        bool ignoreMissing,
                                        Utils::SynchronizedValue<SyncedData>::unique_lock *lock);

    void updateLibraryInfo(const Utils::FilePath &path,
                           const QmlJS::LibraryInfo &info,
                           Utils::SynchronizedValue<SyncedData>::unique_lock &lock);
};

} // namespace QmlJS
