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

#include "qmljsimportdependencies.h"
#include "qmljsinterpreter.h"
#include "qmljsviewercontext.h"

#include <utils/algorithm.h>
#include <utils/qrcparser.h>
#include <utils/qtcassert.h>

#include <QCryptographicHash>
#include <QElapsedTimer>
#include <QLoggingCategory>

#include <algorithm>

static Q_LOGGING_CATEGORY(importsLog, "qtc.qmljs.imports", QtWarningMsg)
static Q_LOGGING_CATEGORY(importsBenchmark, "qtc.qmljs.imports.benchmark", QtWarningMsg)


class ImportsBenchmarker
{
public:
    ImportsBenchmarker(const QString &functionName)
        : m_functionName(functionName)
    {
        m_timer.start();
    }

    ~ImportsBenchmarker()
    {
        if (importsBenchmark().isDebugEnabled()) {
            qCDebug(importsBenchmark).noquote().nospace() << m_functionName << " executed nPossibleExports: " << nPossibleExports << " in " << m_timer.elapsed() << "ms";
        }
    }
    int nPossibleExports = 0;

    QElapsedTimer m_timer;
    QString m_functionName;
};

namespace QmlJS {

/*
 which languages might be imported in this context
 */
static bool languageIsCompatible(Dialect contextLanguage, Dialect importLanguage)
{
    if (importLanguage == Dialect::AnyLanguage && contextLanguage != Dialect::NoLanguage)
        return true;
    switch (contextLanguage.dialect()) {
    case Dialect::JavaScript:
    case Dialect::Json:
    case Dialect::QmlProject:
    case Dialect::QmlQbs:
    case Dialect::QmlTypeInfo:
        return contextLanguage == importLanguage;
    case Dialect::Qml:
        return importLanguage == Dialect::Qml || importLanguage == Dialect::QmlQtQuick2 || importLanguage == Dialect::JavaScript;
    case Dialect::QmlQtQuick2:
    case Dialect::QmlQtQuick2Ui:
        return importLanguage == Dialect::Qml || importLanguage == Dialect::QmlQtQuick2 || importLanguage == Dialect::QmlQtQuick2Ui
            || importLanguage == Dialect::JavaScript;
    case Dialect::AnyLanguage:
        return true;
    case Dialect::NoLanguage:
        break;
    }
    return false;
}

ImportKind::Enum toImportKind(ImportType::Enum type)
{
    switch (type) {
    case ImportType::Invalid:
        break;
    case ImportType::Library:
        return ImportKind::Library;
    case ImportType::ImplicitDirectory:
    case ImportType::File:
    case ImportType::Directory:
    case ImportType::UnknownFile:
        return ImportKind::Path;
    case ImportType::QrcFile:
    case ImportType::QrcDirectory:
        return ImportKind::QrcPath;
    }
    return ImportKind::Invalid;
}

ImportMatchStrength::ImportMatchStrength(const QList<int> &match)
    : m_match(match)
{ }

int ImportMatchStrength::compareMatch(const ImportMatchStrength &o) const
{
    int len1 = m_match.size();
    int len2 = o.m_match.size();
    int len = ((len1 < len2) ? len1 : len2);
    for (int i = 0; i < len; ++ i) {
        int v1 = m_match.at(i);
        int v2 = o.m_match.at(i);
        if (v1 < v2)
            return -1;
        if (v1 > v2)
            return 1;
    }
    if (len1 < len2)
        return -1;
    if (len1 > len2)
        return 1;
    return 0;
}

bool ImportMatchStrength::hasNoMatch()
{
    return m_match.isEmpty();
}

bool ImportMatchStrength::hasMatch()
{
    return !m_match.isEmpty();
}

bool operator ==(const ImportMatchStrength &m1, const ImportMatchStrength &m2)
{
    return m1.m_match == m2.m_match;
}

bool operator !=(const ImportMatchStrength &m1, const ImportMatchStrength &m2)
{
    return !(m1 == m2);
}

bool operator <(const ImportMatchStrength &m1, const ImportMatchStrength &m2)
{
    return m1.compareMatch(m2) < 0;
}

ImportKey::ImportKey()
    : type(ImportType::Invalid),
      majorVersion(LanguageUtils::ComponentVersion::NoVersion),
      minorVersion(LanguageUtils::ComponentVersion::NoVersion)
{ }

ImportKey::ImportKey(const ImportInfo &info)
    : type(info.type())
    , majorVersion(info.version().majorVersion())
    , minorVersion(info.version().minorVersion())
{
    splitPath = QFileInfo(info.path()).canonicalFilePath().split('/');
}

ImportKey::ImportKey(ImportType::Enum type, const QString &path, int majorVersion, int minorVersion)
    : type(type)
    , majorVersion(majorVersion)
    , minorVersion(minorVersion)
{
    switch (type) {
    case ImportType::Library:
        splitPath = path.split(QLatin1Char('.'));
        break;
    case ImportType::ImplicitDirectory:
    case ImportType::Directory:
        splitPath = path.split(QLatin1Char('/'));
        if (splitPath.length() > 1 && splitPath.last().isEmpty())
            splitPath.removeLast();
        break;
    case ImportType::File:
    case ImportType::QrcFile:
        splitPath = Utils::QrcParser::normalizedQrcFilePath(path).split(QLatin1Char('/'));
        break;
    case ImportType::QrcDirectory:
        splitPath = Utils::QrcParser::normalizedQrcDirectoryPath(path).split(QLatin1Char('/'));
        if (splitPath.length() > 1 && splitPath.last().isEmpty())
            splitPath.removeLast();
        break;
    case ImportType::Invalid:
    case ImportType::UnknownFile:
        splitPath = path.split(QLatin1Char('/'));
        break;
    }
}

void ImportKey::addToHash(QCryptographicHash &hash) const
{
    hash.addData(QByteArrayView(reinterpret_cast<const char *>(&type), sizeof(type)));
    hash.addData(QByteArrayView(reinterpret_cast<const char *>(&majorVersion), sizeof(majorVersion)));
    hash.addData(QByteArrayView(reinterpret_cast<const char *>(&minorVersion), sizeof(minorVersion)));
    for (const QString &s : splitPath) {
        hash.addData("/");
        hash.addData(QByteArrayView(reinterpret_cast<const char *>(s.constData()), sizeof(QChar) * s.size()));
    }
    hash.addData("/");
}

ImportKey ImportKey::flatKey() const {
    switch (type) {
    case ImportType::Invalid:
        return *this;
    case ImportType::ImplicitDirectory:
    case ImportType::Library:
    case ImportType::File:
    case ImportType::Directory:
    case ImportType::QrcFile:
    case ImportType::QrcDirectory:
    case ImportType::UnknownFile:
        break;
    }
    QStringList flatPath = splitPath;
    int i = 0;
    while (i < flatPath.size()) {
        if (flatPath.at(i).startsWith(QLatin1Char('+')))
            flatPath.removeAt(i);
        else
            ++i;
    }
    if (flatPath.size() == splitPath.size())
        return *this;
    ImportKey res = *this;
    res.splitPath = flatPath;
    return res;
}

QString ImportKey::libraryQualifiedPath() const
{
    QString res = splitPath.join(QLatin1Char('.'));
    if (res.isEmpty() && !splitPath.isEmpty())
        return QLatin1String("");
    return res;
}

QString ImportKey::path() const
{
    QString res = splitPath.join(QLatin1Char('/'));
    if (res.isEmpty() && !splitPath.isEmpty())
        return QLatin1String("/");
    return res;
}

ImportMatchStrength ImportKey::matchImport(const ImportKey &o, const ViewerContext &vContext) const
{
    if (majorVersion != o.majorVersion || minorVersion > o.minorVersion)
        return ImportMatchStrength();
    bool dirToFile = false;
    switch (o.type) {
    case ImportType::Invalid:
        return ImportMatchStrength();
    case ImportType::ImplicitDirectory:
    case ImportType::Directory:
        switch (type) {
        case ImportType::File:
        case ImportType::UnknownFile:
            dirToFile = true;
            break;
        case ImportType::ImplicitDirectory:
        case ImportType::Directory:
            break;
        default:
            return ImportMatchStrength();
        }
        break;
    case ImportType::Library:
        if (type != ImportType::Library)
            return ImportMatchStrength();
        break;
    case ImportType::QrcDirectory:
        switch (type) {
        case ImportType::QrcFile:
            dirToFile = true;
            break;
        case ImportType::QrcDirectory:
            break;
        default:
            return ImportMatchStrength();
        }
        break;
    case ImportType::QrcFile:
        if (type != ImportType::QrcFile)
            return ImportMatchStrength();
        break;
    case ImportType::UnknownFile:
    case ImportType::File:
        switch (type) {
        case ImportType::UnknownFile:
        case ImportType::File:
            break;
        default:
            return ImportMatchStrength();
        }
        break;
    }

    QList<int> res;
    int iPath1 = 0;
    int lenPath1 = splitPath.size();
    int iPath2 = 0;
    int lenPath2 = o.splitPath.size();
    if (dirToFile)
        --lenPath1;
    int iSelector = 0;
    const int nSelectors = vContext.selectors.size();
    while (iPath1 < lenPath1) {
        if (lenPath2 - iPath2 > lenPath1 - iPath1)
            return ImportMatchStrength();
        const QString p1 = splitPath.at(iPath1);
        if (iPath2 < lenPath2) {
            const QString p2 = o.splitPath.at(iPath2);
            if (p1 == p2) {
                ++iPath1;
                ++iPath2;
                continue;
            }
        }
        if (!p1.startsWith(QLatin1Char('+')))
            return ImportMatchStrength();
        const QStringView selectorAtt(p1.constData() + 1, p1.size() - 1);
        while (iSelector < nSelectors) {
            if (selectorAtt == vContext.selectors.at(iSelector))
                break;
            ++iSelector;
        }
        if (iSelector == nSelectors)
            return ImportMatchStrength();
        res << (nSelectors - iSelector);
        ++iSelector;
        ++iPath1;
    }
    if (iPath2 != lenPath2)
        return ImportMatchStrength();
    if (res.isEmpty())
        res << 0;
    return ImportMatchStrength(res);
}

int ImportKey::compare(const ImportKey &other) const
{
    ImportKind::Enum k1 = toImportKind(type);
    ImportKind::Enum k2 = toImportKind(other.type);
    if (k1 < k2)
        return -1;
    if (k1 > k2)
        return 1;
    int len1 = splitPath.size();
    int len2 = other.splitPath.size();
    int len = ((len1 < len2) ? len1 : len2);
    for (int i = 0; i < len; ++ i) {
        QString v1 = splitPath.at(i);
        QString v2 = other.splitPath.at(i);
        if (v1 < v2)
            return -1;
        if (v1 > v2)
            return 1;
    }
    if (len1 < len2)
        return -1;
    if (len1 > len2)
        return 1;
    if (majorVersion < other.majorVersion)
        return -1;
    if (majorVersion > other.majorVersion)
        return 1;
    if (minorVersion < other.minorVersion)
        return -1;
    if (minorVersion > other.minorVersion)
        return 1;
    if (type < other.type)
        return -1;
    if (type > other.type)
        return 1;
    return 0;
}

bool ImportKey::isDirectoryLike() const
{
    switch (type) {
    case ImportType::Directory:
    case ImportType::ImplicitDirectory:
    case ImportType::QrcDirectory:
        return true;
    default:
        return false;
    }
}

ImportKey::DirCompareInfo ImportKey::compareDir(const ImportKey &superDir) const
{
    // assumes dir/+selectors/file (i.e. no directories inside selectors)
    switch (superDir.type) {
    case ImportType::UnknownFile:
    case ImportType::File:
    case ImportType::Directory:
    case ImportType::ImplicitDirectory:
        if (type != ImportType::File && type != ImportType::ImplicitDirectory
                && type != ImportType::Directory && type != ImportType::UnknownFile)
            return Incompatible;
        break;
    case ImportType::QrcDirectory:
    case ImportType::QrcFile:
        if (type != ImportType::QrcDirectory && type != ImportType::QrcFile)
            return Incompatible;
        break;
    case ImportType::Invalid:
    case ImportType::Library:
        return Incompatible;
    }
    bool isDir1 = isDirectoryLike();
    bool isDir2 = superDir.isDirectoryLike();
    int len1 = splitPath.size();
    int len2 = superDir.splitPath.size();
    if (isDir1 && len1 > 0)
        --len1;
    if (isDir2 && len2 > 0)
        --len2;

    int i1 = 0;
    int i2 = 0;
    while (i1 < len1 && i2 < len2) {
        QString p1 = splitPath.at(i1);
        QString p2 = superDir.splitPath.at(i2);
        if (p1 == p2) {
            ++i1;
            ++i2;
            continue;
        }
        if (p1.startsWith(QLatin1Char('+'))) {
            if (p2.startsWith(QLatin1Char('+')))
                return SameDir;
            return SecondInFirst;
        }
        if (p2.startsWith(QLatin1Char('+')))
            return FirstInSecond;
        return Different;
    }
    if (i1 < len1) {
        if (splitPath.at(i1).startsWith(QLatin1Char('+')))
            return SameDir;
        return SecondInFirst;
    }
    if (i2 < len2) {
        if (superDir.splitPath.at(i2).startsWith(QLatin1Char('+')))
            return SameDir;
        return SecondInFirst;
    }
    return SameDir;
}

QString ImportKey::toString() const
{
    QString res;
    switch (type) {
    case ImportType::UnknownFile:
    case ImportType::File:
        res = path();
        break;
    case ImportType::Directory:
    case ImportType::ImplicitDirectory:
        res = path() + QLatin1Char('/');
        break;
    case ImportType::QrcDirectory:
        res = QLatin1String("qrc:") + path() + QLatin1Char('/');
        break;
    case ImportType::QrcFile:
        res = QLatin1String("qrc:") + path() + QLatin1Char('/');
        break;
    case ImportType::Invalid:
        res = path();
        break;
    case ImportType::Library:
        res = splitPath.join(QLatin1Char('.'));
        break;
    }

    if (majorVersion != LanguageUtils::ComponentVersion::NoVersion
            || minorVersion != LanguageUtils::ComponentVersion::NoVersion)
        return res + QLatin1Char(' ') + QString::number(majorVersion)
                + QLatin1Char('.') + QString::number(minorVersion);

    return res;
}

size_t qHash(const ImportKey &info)
{
    size_t res = ::qHash(info.type) ^
            ::qHash(info.majorVersion) ^ ::qHash(info.minorVersion);
    for (const QString &s : std::as_const(info.splitPath))
        res = res ^ ::qHash(s);
    return res;
}

bool operator==(const ImportKey &i1, const ImportKey &i2)
{
    return i1.type == i2.type
            && i1.splitPath == i2.splitPath
            && i1.majorVersion == i2.majorVersion
            && i1.minorVersion == i2.minorVersion;
}

bool operator !=(const ImportKey &i1, const ImportKey &i2)
{
    return ! (i1 == i2);
}

bool operator <(const ImportKey &i1, const ImportKey &i2)
{
    return i1.compare(i2) < 0;
}

QString Export::libraryTypeName() { return QStringLiteral("%Library%"); }

Export::Export()
    : intrinsic(false)
{ }

Export::Export(ImportKey exportName,
               const Utils::FilePath &pathRequired,
               bool intrinsic,
               const QString &typeName)
    : exportName(exportName)
    , pathRequired(pathRequired)
    , typeName(typeName)
    , intrinsic(intrinsic)
{ }

bool Export::visibleInVContext(const ViewerContext &vContext) const
{
    return pathRequired.isEmpty() || vContext.paths.count(pathRequired);
}

CoreImport::CoreImport() : language(Dialect::Qml) { }

CoreImport::CoreImport(const QString &importId, const QList<Export> &possibleExports,
                       Dialect language, const QByteArray &fingerprint)
    : importId(importId), possibleExports(possibleExports), language(language),
      fingerprint(fingerprint)
{ }

bool CoreImport::valid() {
    return !fingerprint.isEmpty();
}

QByteArray DependencyInfo::calculateFingerprint(const ImportDependencies &deps)
{
    QCryptographicHash hash(QCryptographicHash::Sha1);
    rootImport.addToHash(hash);
    QStringList coreImports = Utils::toList(allCoreImports);
    coreImports.sort();
    for (const QString &importId : std::as_const(coreImports)) {
        hash.addData(QByteArrayView(reinterpret_cast<const char*>(importId.constData()), importId.size() * sizeof(QChar)));
        QByteArray coreImportFingerprint = deps.coreImport(importId).fingerprint;
        hash.addData(coreImportFingerprint);
    }
    hash.addData("/");
    QList<ImportKey> imports = Utils::toList(allImports);
    std::sort(imports.begin(), imports.end());
    for (const ImportKey &k : std::as_const(imports))
        k.addToHash(hash);
    return hash.result();
}

MatchedImport::MatchedImport()
{ }

MatchedImport::MatchedImport(ImportMatchStrength matchStrength, ImportKey importKey,
                             const QString &coreImportId)
    : matchStrength(matchStrength), importKey(importKey), coreImportId(coreImportId)
{ }

int MatchedImport::compare(const MatchedImport &o) const {
    int res = matchStrength.compareMatch(o.matchStrength);
    if (res != 0)
        return res;
    res = importKey.compare(o.importKey);
    if (res != 0)
        return res;
    if (coreImportId < o.coreImportId)
        return -1;
    if (coreImportId > o.coreImportId)
        return 1;
    return 0;
}

bool operator ==(const MatchedImport &m1, const MatchedImport &m2)
{
    return m1.compare(m2) == 0;
}

bool operator !=(const MatchedImport &m1, const MatchedImport &m2)
{
    return m1.compare(m2) != 0;
}

bool operator <(const MatchedImport &m1, const MatchedImport &m2)
{
    return m1.compare(m2) < 0;
}

ImportDependencies::ImportDependencies()
{ }

ImportDependencies::~ImportDependencies()
{ }

void ImportDependencies::filter(const ViewerContext &vContext)
{
    ImportsBenchmarker benchMark("filter()");
    QMap<QString, CoreImport> newCoreImports;
    QMap<ImportKey, QStringList> newImportCache;
    bool hasChanges = false;
    for (auto j = m_coreImports.cbegin(), end = m_coreImports.cend(); j != end; ++j) {
        const CoreImport &cImport = j.value();
        if (languageIsCompatible(vContext.language, cImport.language)) {
            QList<Export> newExports;
            for (const Export &e : std::as_const(cImport.possibleExports)) {
                ++benchMark.nPossibleExports;
                if (e.visibleInVContext(vContext)) {
                    newExports.append(e);
                    QStringList &candidateImports = newImportCache[e.exportName];
                    if (!candidateImports.contains(cImport.importId))
                        candidateImports.append(cImport.importId);
                }
            }
            if (newExports.size() == cImport.possibleExports.size()) {
                newCoreImports.insert(cImport.importId, cImport);
            } else if (newExports.length() > 0) {
                CoreImport newCImport = cImport;
                newCImport.possibleExports = newExports;
                newCoreImports.insert(newCImport.importId, newCImport);
                hasChanges = true;
            } else {
                hasChanges = true;
            }
        } else {
            hasChanges = true;
        }
    }
    if (!hasChanges)
        return;
    m_coreImports = newCoreImports;
    m_importCache = newImportCache;
}

CoreImport ImportDependencies::coreImport(const QString &importId) const
{
    return m_coreImports.value(importId);
}

void ImportDependencies::iterateOnCandidateImports(
        const ImportKey &key, const ViewerContext &vContext,
        std::function<bool (const ImportMatchStrength &,const Export &,const CoreImport &)>
        const &iterF) const
{
    ImportsBenchmarker benchMark("iterateOnCandidateImports()");
    switch (key.type) {
    case ImportType::Directory:
    case ImportType::QrcDirectory:
    case ImportType::ImplicitDirectory:
        break;
    default:
    {
        const QStringList imp = m_importCache.value(key.flatKey());
        for (const QString &cImportName : imp) {
            CoreImport cImport = coreImport(cImportName);
            if (languageIsCompatible(vContext.language, cImport.language)) {
                for (const Export &e : std::as_const(cImport.possibleExports)) {
                    ++benchMark.nPossibleExports;
                    if (e.visibleInVContext(vContext)) {
                        ImportMatchStrength m = e.exportName.matchImport(key, vContext);
                        if (m.hasMatch()) {
                            if (!iterF(m, e, cImport))
                                return;
                        }
                    }
                }
            }
        }
        return;
    }
    }
    QMap<ImportKey, QStringList>::const_iterator lb = m_importCache.lowerBound(key.flatKey());
    QMap<ImportKey, QStringList>::const_iterator end = m_importCache.constEnd();
    while (lb != end) {
        ImportKey::DirCompareInfo c = key.compareDir(lb.key());
        if (c == ImportKey::SameDir) {
            for (const QString &cImportName : std::as_const(lb.value())) {
                CoreImport cImport = coreImport(cImportName);
                if (languageIsCompatible(vContext.language, cImport.language)) {
                    for (const Export &e : std::as_const(cImport.possibleExports)) {
                        ++benchMark.nPossibleExports;
                        if (e.visibleInVContext(vContext)) {
                            ImportMatchStrength m = e.exportName.matchImport(key, vContext);
                            if (m.hasMatch()) {
                                if (!iterF(m, e, cImport))
                                    return;
                            }
                        }
                    }
                }
            }
        } else if (c != ImportKey::SecondInFirst) {
            break;
        }
        ++lb;
    }
}

class CollectCandidateImports
{
public:
    ImportDependencies::ImportElements &res;

    CollectCandidateImports(ImportDependencies::ImportElements & res)
        : res(res)
    { }

    bool operator ()(const ImportMatchStrength &m, const Export &e, const CoreImport &cI) const
    {
        ImportKey flatName = e.exportName.flatKey();
        res[flatName].append(MatchedImport(m, e.exportName, cI.importId));
        return true;
    }
};

ImportDependencies::ImportElements ImportDependencies::candidateImports(
        const ImportKey &key,
        const ViewerContext &vContext) const
{
    ImportDependencies::ImportElements res;
    CollectCandidateImports collector(res);
    iterateOnCandidateImports(key, vContext, collector);
    typedef QMap<ImportKey, QList<MatchedImport> >::iterator iter_t;
    iter_t i = res.begin();
    iter_t end = res.end();
    while (i != end) {
        std::sort(i.value().begin(), i.value().end());
        ++i;
    }
    return res;
}

QList<DependencyInfo::ConstPtr> ImportDependencies::createDependencyInfos(
        const ImportKey &mainDoc, const ViewerContext &vContext) const
{
    Q_UNUSED(mainDoc)
    Q_UNUSED(vContext)
    QList<DependencyInfo::ConstPtr> res;
    QTC_CHECK(false);
    return res;
}

void ImportDependencies::addCoreImport(const CoreImport &import)
{
    CoreImport newImport = import;
    if (m_coreImports.contains(import.importId)) {
        CoreImport oldVal = m_coreImports.value(import.importId);
        for (const Export &e : std::as_const(oldVal.possibleExports)) {
            if (e.intrinsic)
                removeImportCacheEntry(e.exportName, import.importId);
            else
                newImport.possibleExports.append(e);
        }
    }
    for (const Export &e : std::as_const(import.possibleExports))
        m_importCache[e.exportName].append(import.importId);
    m_coreImports.insert(newImport.importId, newImport);
    if (importsLog().isDebugEnabled()) {
        QString msg = QString::fromLatin1("added import %1 for").arg(newImport.importId);
        for (const Export &e : std::as_const(newImport.possibleExports))
            msg += QString::fromLatin1("\n %1(%2)")
                       .arg(e.exportName.toString(), e.pathRequired.toUserOutput());
        qCDebug(importsLog) << msg;
    }
}

void ImportDependencies::removeCoreImport(const QString &importId)
{
    if (!m_coreImports.contains(importId)) {
        qCWarning(importsLog) << "missing importId in removeCoreImport(" << importId << ")";
        return;
    }
    CoreImport &cImport = m_coreImports[importId];
    QList<Export> newExports;
    for (const Export &e : std::as_const(cImport.possibleExports))
        if (e.intrinsic)
            removeImportCacheEntry(e.exportName, importId);
        else
            newExports.append(e);
    if (newExports.size()>0)
        cImport.possibleExports = newExports;
    else
        m_coreImports.remove(importId);

    qCDebug(importsLog) << "removed import with id:"<< importId;
}

void ImportDependencies::removeImportCacheEntry(const ImportKey &importKey, const QString &importId)
{
    QStringList &cImp = m_importCache[importKey];
    if (!cImp.removeOne(importId)) {
        qCWarning(importsLog) << "missing possibleExport backpointer for " << importKey.toString() << " to "
                              << importId;
    }
    if (cImp.isEmpty())
        m_importCache.remove(importKey);
}

void ImportDependencies::addExport(const QString &importId,
                                   const ImportKey &importKey,
                                   const Utils::FilePath &requiredPath,
                                   const QString &typeName)
{
    if (!m_coreImports.contains(importId)) {
        CoreImport newImport(importId);
        newImport.language = Dialect::AnyLanguage;
        newImport.addPossibleExport(Export(importKey, requiredPath, false, typeName));
        m_coreImports.insert(newImport.importId, newImport);
        m_importCache[importKey].append(importId);
        return;
    }
    CoreImport &importValue = m_coreImports[importId];
    importValue.addPossibleExport(Export(importKey, requiredPath, false, typeName));
    m_importCache[importKey].append(importId);
    qCDebug(importsLog) << "added export "<< importKey.toString() << " for id " <<importId
                        << " (" << requiredPath << ")";
}

void ImportDependencies::removeExport(const QString &importId,
                                      const ImportKey &importKey,
                                      const Utils::FilePath &requiredPath,
                                      const QString &typeName)
{
    if (!m_coreImports.contains(importId)) {
        qCWarning(importsLog) << "non existing core import for removeExport(" << importId << ", "
                              << importKey.toString() << ")";
    } else {
        CoreImport &importValue = m_coreImports[importId];
        if (!importValue.possibleExports.removeOne(Export(importKey, requiredPath, false, typeName))) {
            qCWarning(importsLog) << "non existing export for removeExport(" << importId << ", "
                                  << importKey.toString() << ")";
        }
        if (importValue.possibleExports.isEmpty() && importValue.fingerprint.isEmpty())
            m_coreImports.remove(importId);
    }
    if (!m_importCache.contains(importKey)) {
        qCWarning(importsLog) << "missing possibleExport for " << importKey.toString()
                            << " when removing export of " << importId;
    } else {
        removeImportCacheEntry(importKey, importId);
    }
    qCDebug(importsLog) << "removed export "<< importKey.toString() << " for id " << importId
                        << " (" << requiredPath << ")";
}

void ImportDependencies::iterateOnLibraryImports(
        const ViewerContext &vContext,
        std::function<bool (const ImportMatchStrength &,
                            const Export &,
                            const CoreImport &)> const &iterF) const
{
    ImportsBenchmarker benchMark("iterateOnLibraryImports()");

    typedef QMap<ImportKey, QStringList>::const_iterator iter_t;
    ImportKey firstLib;
    firstLib.type = ImportType::Library;
    iter_t i = m_importCache.lowerBound(firstLib);
    iter_t end = m_importCache.constEnd();
    while (i != end && i.key().type == ImportType::Library) {
        qCDebug(importsLog) << "libloop:" << i.key().toString() << i.value();
        for (const QString &cImportName : i.value()) {
            CoreImport cImport = coreImport(cImportName);
            if (languageIsCompatible(vContext.language, cImport.language)) {
                for (const Export &e : std::as_const(cImport.possibleExports)) {
                    ++benchMark.nPossibleExports;
                    if (e.visibleInVContext(vContext) && e.exportName.type == ImportType::Library) {
                        ImportMatchStrength m = e.exportName.matchImport(i.key(), vContext);
                        if (m.hasMatch()) {
                            qCDebug(importsLog) << "import iterate:" << e.exportName.toString()
                                                << " (" << e.pathRequired << "), id:" << cImport.importId;
                            if (!iterF(m, e, cImport))
                                return;
                        }
                    }
                }
            }
        }
        ++i;
    }
}

void ImportDependencies::iterateOnSubImports(
        const ImportKey &baseKey,
        const ViewerContext &vContext,
        std::function<bool (const ImportMatchStrength &,
                            const Export &,
                            const CoreImport &)> const &iterF) const
{
    ImportsBenchmarker benchMark("iterateOnSubImports()");
    typedef QMap<ImportKey, QStringList>::const_iterator iter_t;
    iter_t i = m_importCache.lowerBound(baseKey);
    iter_t end = m_importCache.constEnd();
    while (i != end) {
        ImportKey::DirCompareInfo c = baseKey.compareDir(i.key());
        if (c != ImportKey::SameDir && c != ImportKey::SecondInFirst)
            break;
        for (const QString &cImportName : i.value()) {
            CoreImport cImport = coreImport(cImportName);
            if (languageIsCompatible(vContext.language, cImport.language)) {
                for (const Export &e : std::as_const(cImport.possibleExports)) {
                    ++benchMark.nPossibleExports;
                    if (e.visibleInVContext(vContext)) {
                        ImportMatchStrength m = e.exportName.matchImport(i.key(), vContext);
                        if (m.hasMatch()) {
                            if (!iterF(m, e, cImport))
                                return;
                        }
                    }
                }
            }
        }
        ++i;
    }
}

class CollectImportKeys {
public:
    QSet<ImportKey> &imports;
    CollectImportKeys(QSet<ImportKey> &imports)
        : imports(imports)
    { }
    bool operator()(const ImportMatchStrength &m,
                    const Export &e,
                    const CoreImport &cI) const
    {
        Q_UNUSED(m)
        Q_UNUSED(cI)
        imports.insert(e.exportName.flatKey());
        return true;
    }
};

QSet<ImportKey> ImportDependencies::libraryImports(const ViewerContext &viewContext) const
{
    QSet<ImportKey> res;
    CollectImportKeys importCollector(res);
    iterateOnLibraryImports(viewContext, importCollector);
    return res;
}

QSet<ImportKey> ImportDependencies::subdirImports(
        const ImportKey &baseKey, const ViewerContext &viewContext) const
{
    QSet<ImportKey> res;
    CollectImportKeys importCollector(res);
    iterateOnSubImports(baseKey, viewContext, importCollector);
    return res;
}

void ImportDependencies::checkConsistency() const
{
    for (auto j = m_importCache.cbegin(), end = m_importCache.cend(); j != end; ++j) {
        for (const QString &s : j.value()) {
            bool found = false;
            const QList<Export> exports = m_coreImports.value(s).possibleExports;
            for (const Export &e : exports)
                if (e.exportName == j.key())
                    found = true;
            Q_ASSERT(found); Q_UNUSED(found)
        }
    }
    for (auto i = m_coreImports.cbegin(), end = m_coreImports.cend(); i != end; ++i) {
        for (const Export &e : std::as_const(i.value().possibleExports)) {
            if (!m_importCache.value(e.exportName).contains(i.key())) {
                qCWarning(importsLog) << e.exportName.toString();
                qCWarning(importsLog) << i.key();

                for (auto j = m_importCache.cbegin(), end = m_importCache.cend(); j != end; ++j)
                    qCWarning(importsLog) << j.key().toString() << j.value();

                qCWarning(importsLog) << m_importCache.contains(e.exportName);
                qCWarning(importsLog) << m_importCache.value(e.exportName);
            }
            Q_ASSERT(m_importCache.value(e.exportName).contains(i.key()));
        }
    }
}

} // namespace QmlJS
