/****************************************************************************
**
** Copyright (C) 2023 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qbs.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "dependenciesresolver.h"

#include "itemreader.h"
#include "loaderutils.h"
#include "moduleinstantiator.h"
#include "moduleloader.h"

#include <language/scriptengine.h>
#include <language/evaluator.h>
#include <language/item.h>
#include <language/itempool.h>
#include <language/value.h>
#include <logging/categories.h>
#include <logging/translator.h>
#include <tools/fileinfo.h>
#include <tools/preferences.h>
#include <tools/profiling.h>
#include <tools/settings.h>
#include <tools/setupprojectparameters.h>
#include <tools/stringconstants.h>

#include <optional>
#include <queue>
#include <unordered_map>

namespace qbs::Internal {
namespace {
enum class HandleDependency { Use, Ignore, Defer };

class LoadModuleResult
{
public:
    Item *moduleItem = nullptr;
    ProductContext *product = nullptr;
    HandleDependency handleDependency = HandleDependency::Use;
};

// Corresponds completely to a Depends item.
// May result in more than one module, due to "multiplexing" properties such as subModules etc.
// May also result in no module at all, e.g. if productTypes does not match anything.
class EvaluatedDependsItem
{
public:
    Item *item = nullptr;
    QualifiedId name;
    QStringList subModules;
    FileTags productTypes;
    QStringList multiplexIds;
    std::optional<QStringList> profiles;
    VersionRange versionRange;
    QVariantMap parameters;
    bool limitToSubProject = false;
    FallbackMode fallbackMode = FallbackMode::Enabled;
    bool requiredLocally = true;
    bool requiredGlobally = true;
};

// As opposed to EvaluatedDependsItem, one of these corresponds exactly to one module
// to be loaded. Such an attempt might still fail, though, which may or may not result
// in an error, depending on the value of Depends.required and other circumstances.
class FullyResolvedDependsItem
{
public:
    FullyResolvedDependsItem(ProductContext *product, const EvaluatedDependsItem &dependency);
    FullyResolvedDependsItem(const EvaluatedDependsItem &dependency, QualifiedId name,
                             QString profile, QString multiplexId);
    FullyResolvedDependsItem() = default;
    static FullyResolvedDependsItem makeBaseDependency();

    QString id() const;
    CodeLocation location() const;
    QString displayName() const;

    // If product is non-null, we already know which product the dependency targets.
    // This happens either if Depends.productTypes was set, or if we tried to load the
    // dependency before and already identified the product, but could not complete the
    // procedure because said product had itself not been handled yet.
    ProductContext *product = nullptr;

    Item *item = nullptr;
    QualifiedId name;
    QString profile;
    QString multiplexId;
    VersionRange versionRange;
    QVariantMap parameters;
    bool limitToSubProject = false;
    FallbackMode fallbackMode = FallbackMode::Enabled;
    bool requiredLocally = true;
    bool requiredGlobally = true;
    bool checkProduct = true;
};

class DependenciesResolvingState
{
public:
    Item *loadingItem = nullptr;
    FullyResolvedDependsItem loadingItemOrigin;
    std::queue<Item *> pendingDependsItems;
    std::optional<EvaluatedDependsItem> currentDependsItem;
    std::queue<FullyResolvedDependsItem> pendingResolvedDependencies;
    bool requiredByLoadingItem = true;
};

class DependenciesContextImpl : public DependenciesContext
{
public:
    DependenciesContextImpl(ProductContext &product, LoaderState &loaderState);

    std::list<DependenciesResolvingState> stateStack;

private:
    std::pair<ProductDependency, ProductContext *> pendingDependency() const override;

    void setSearchPathsForProduct(LoaderState &loaderState);

    ProductContext &m_product;
};

class DependenciesResolver
{
public:
    DependenciesResolver(LoaderState &loaderState, ProductContext &product, Deferral deferral)
        : m_loaderState(loaderState), m_product(product), m_deferral(deferral) {}

    void resolve();
    LoadModuleResult loadModule(Item *loadingItem, const FullyResolvedDependsItem &dependency);

private:
    void evaluateNextDependsItem();
    HandleDependency handleResolvedDependencies();
    std::pair<Item::Module *, Item *> findExistingModule(const FullyResolvedDependsItem &dependency,
                                                         Item *item);
    void updateModule(Item::Module &module, const FullyResolvedDependsItem &dependency);
    int dependsChainLength();
    ProductContext *findMatchingProduct(const FullyResolvedDependsItem &dependency);
    Item *findMatchingModule(const FullyResolvedDependsItem &dependency);
    std::pair<bool, HandleDependency> checkProductDependency(
        const FullyResolvedDependsItem &depSpec, const ProductContext &dep);
    void checkModule(const FullyResolvedDependsItem &dependency, Item *moduleItem,
                     ProductContext *productDep);
    void checkForModuleNamePrefixCollision(const FullyResolvedDependsItem &dependency);
    Item::Module createModule(const FullyResolvedDependsItem &dependency, Item *item,
                              ProductContext *productDep);
    void adjustDependsItemForMultiplexing(Item *dependsItem);
    std::optional<EvaluatedDependsItem> evaluateDependsItem(Item *item);
    std::queue<FullyResolvedDependsItem> multiplexDependency(
        const EvaluatedDependsItem &dependency);
    QVariantMap extractParameters(Item *dependsItem) const;
    void forwardParameterDeclarations(const Item *dependsItem, const Item::Modules &modules);
    void forwardParameterDeclarations(const QualifiedId &moduleName, Item *item,
                                      const Item::Modules &modules);
    std::list<DependenciesResolvingState> &stateStack();

    LoaderState &m_loaderState;
    ProductContext &m_product;
    Deferral m_deferral;
};

static bool haveSameSubProject(const ProductContext &p1, const ProductContext &p2);
static QVariantMap safeToVariant(JSContext *ctx, const JSValue &v);

} // namespace

void resolveDependencies(ProductContext &product, Deferral deferral, LoaderState &loaderState)
{
    DependenciesResolver(loaderState, product, deferral).resolve();
}

Item *loadBaseModule(ProductContext &product, Item *item, LoaderState &loaderState)
{
    const auto baseDependency = FullyResolvedDependsItem::makeBaseDependency();
    Item * const moduleItem = DependenciesResolver(loaderState, product, Deferral::NotAllowed)
            .loadModule(item, baseDependency).moduleItem;
    if (Q_UNLIKELY(!moduleItem))
        throw ErrorInfo(Tr::tr("Cannot load base qbs module."));
    return moduleItem;
}

namespace {

void DependenciesResolver::resolve()
{
    AccumulatingTimer timer(m_loaderState.parameters().logElapsedTime()
                            ? &m_product.timingData.dependenciesResolving : nullptr);

    if (!m_product.dependenciesContext) {
        m_product.dependenciesContext = std::make_unique<DependenciesContextImpl>(
                    m_product, m_loaderState);
    } else {
        QBS_CHECK(!m_product.dependenciesContext->dependenciesResolved);
    }
    SearchPathsManager searchPathsMgr(m_loaderState.itemReader(), m_product.searchPaths);

    while (!stateStack().empty()) {
        auto &state = stateStack().front();

        // If we have pending FullyResolvedDependsItems, then these are handled first.
        if (handleResolvedDependencies() == HandleDependency::Defer)
            return;

        // The above procedure might have pushed another state to the stack due to recursive
        // dependencies (i.e. Depends items in the newly loaded module), in which case we
        // continue with that one.
        if (&state != &stateStack().front())
            continue;

        // If we have a pending EvaluatedDependsItem, we multiplex it and then handle
        // the resulting FullyResolvedDependsItems, if there were any.
        if (state.currentDependsItem) {
            QBS_CHECK(state.pendingResolvedDependencies.empty());

            // We postpone handling Depends.productTypes for as long as possible, because
            // the full type of a product becomes available only after its modules have been loaded.
            if (!state.currentDependsItem->productTypes.empty() && m_deferral == Deferral::Allowed)
                return;

            state.pendingResolvedDependencies = multiplexDependency(*state.currentDependsItem);
            state.currentDependsItem.reset();
            m_deferral = Deferral::Allowed; // We made progress.

            continue;
        }

        // Here we have no resolved/evaluated Depends items of any kind, so we evaluate the next
        // pending Depends item.
        evaluateNextDependsItem();
        if (state.currentDependsItem)
            continue;

        // No resolved or unresolved Depends items are left, so we're done with the current state.
        QBS_CHECK(!state.currentDependsItem);
        QBS_CHECK(state.pendingResolvedDependencies.empty());
        QBS_CHECK(state.pendingDependsItems.empty());

        // This ensures our invariant: A sorted module list in the product
        // (dependers after dependencies).
        if (stateStack().size() > 1) {
            QBS_CHECK(state.loadingItem->type() == ItemType::ModuleInstance);
            Item::Modules &modules = m_product.item->modules();
            const auto loadingItemModule = std::find_if(modules.begin(), modules.end(),
                                                        [&](const Item::Module &m) {
                                                            return m.item == state.loadingItem;
                                                        });
            QBS_CHECK(loadingItemModule != modules.end());
            const Item::Module tempModule = *loadingItemModule;
            modules.erase(loadingItemModule);
            modules.push_back(tempModule);
        }
        stateStack().pop_front();
    }
    m_product.dependenciesContext->dependenciesResolved = true;
}

void DependenciesResolver::evaluateNextDependsItem()
{
    auto &state = stateStack().front();
    while (!state.pendingDependsItems.empty()) {
        QBS_CHECK(!state.currentDependsItem);
        QBS_CHECK(state.pendingResolvedDependencies.empty());
        Item * const dependsItem = state.pendingDependsItems.front();
        state.pendingDependsItems.pop();
        adjustDependsItemForMultiplexing(dependsItem);
        if (auto evaluated = evaluateDependsItem(dependsItem)) {
            evaluated->requiredGlobally = evaluated->requiredLocally
                                          && state.loadingItemOrigin.requiredGlobally;
            state.currentDependsItem = evaluated;
            return;
        }
    }
}

HandleDependency DependenciesResolver::handleResolvedDependencies()
{
    DependenciesResolvingState &state = stateStack().front();
    while (!state.pendingResolvedDependencies.empty()) {
        QBS_CHECK(!state.currentDependsItem);
        const FullyResolvedDependsItem dependency = state.pendingResolvedDependencies.front();
        try {
            const LoadModuleResult res = loadModule(state.loadingItem, dependency);
            switch (res.handleDependency) {
            case HandleDependency::Defer:
                QBS_CHECK(m_deferral == Deferral::Allowed);

                // Optimization: We already looked up the product, so let's not do that again
                // next time.
                if (res.product)
                    state.pendingResolvedDependencies.front().product = res.product;

                return HandleDependency::Defer;
            case HandleDependency::Ignore:
                // This happens if the dependency was not required or the module was already
                // loaded via another path.
                state.pendingResolvedDependencies.pop();
                continue;
            case HandleDependency::Use:
                if (dependency.name.toString() == StringConstants::qbsModule()) {
                    // Shortcut: No need to look for recursive dependencies in the base module.
                    state.pendingResolvedDependencies.pop();
                    continue;
                }
                m_deferral = Deferral::Allowed; // We made progress.
                break;
            }

            QBS_CHECK(res.moduleItem);

            // Now continue with the dependencies of the just-loaded module.
            std::queue<Item *> moduleDependsItems;
            for (Item * const child : res.moduleItem->children()) {
                if (child->type() == ItemType::Depends)
                    moduleDependsItems.push(child);
            }
            state.pendingResolvedDependencies.pop();
            stateStack().push_front(
                {res.moduleItem, dependency, moduleDependsItems, {}, {},
                 dependency.requiredGlobally || state.requiredByLoadingItem});
            stateStack().front().pendingResolvedDependencies.push(
                FullyResolvedDependsItem::makeBaseDependency());
            break;
        } catch (const ErrorInfo &e) {
            if (dependency.name.toString() == StringConstants::qbsModule())
                throw e;

            // This can happen when a property is set unconditionally on a non-required,
            // non-present dependency. We allow this for user convenience.
            if (!dependency.requiredLocally) {
                state.pendingResolvedDependencies.pop();
                continue;
            }

            // See QBS-1338 for why we do not abort handling the product.
            state.pendingResolvedDependencies.pop();
            Item::Modules &modules = m_product.item->modules();

            // Unwind.
            while (stateStack().size() > 1) {
                const auto loadingItemModule = std::find_if(
                    modules.begin(), modules.end(), [&](const Item::Module &m) {
                        return m.item == stateStack().front().loadingItem;
                    });
                for (auto it = loadingItemModule; it != modules.end(); ++it) {
                    createNonPresentModule(m_loaderState.itemPool(), it->name.toString(),
                                           QLatin1String("error in Depends chain"), it->item);
                }
                modules.erase(loadingItemModule, modules.end());
                stateStack().pop_front();
            }

            m_product.handleError(e);
            return HandleDependency::Ignore;
        }
    }
    return HandleDependency::Ignore;
}

// Produces an item of type ModuleInstance corresponding to the specified dependency.
// The instance's prototype item is either of type Export (if the dependency is a product)
// or of type Module (for an actual module).
// The loadingItem parameter is either the depending product or another module. The newly
// created module is added to the module list of the product item and additionally to the
// loading item's one, if it is not the product. Its name is also injected into the respective
// scopes.
LoadModuleResult DependenciesResolver::loadModule(
    Item *loadingItem, const FullyResolvedDependsItem &dependency)
{
    qCDebug(lcModuleLoader) << "loadModule name:" << dependency.name.toString()
                            << "id:" << dependency.id();

    QBS_CHECK(loadingItem);

    ProductContext *productDep = nullptr;
    Item *moduleItem = nullptr;

    const auto addLoadingItem = [&](Item::Module &module, Item &loadingItem) {
        module.loadingItems.emplace_back(&loadingItem,
                                         std::make_pair(dependency.parameters,
                                                        INT_MAX - dependsChainLength()));
    };

    // The module might already have been loaded for this product (directly or indirectly).
    const auto &[existingModule, moduleWithSameName]
            = findExistingModule(dependency, m_product.item);
    if (existingModule) {
        // Merge version range and required property. These will be checked again
        // after probes resolving.
        if (existingModule->name.toString() != StringConstants::qbsModule())
            updateModule(*existingModule, dependency);

        QBS_CHECK(existingModule->item);
        moduleItem = existingModule->item;
        const auto matcher = [loadingItem](const Item::Module::LoadingItemInfo &info) {
            return info.first == loadingItem;
        };
        const auto it = std::find_if(existingModule->loadingItems.begin(),
                                     existingModule->loadingItems.end(), matcher);
        if (it == existingModule->loadingItems.end())
            addLoadingItem(*existingModule, *loadingItem);
        else
            it->second.first = mergeDependencyParameters(it->second.first, dependency.parameters);
    } else if (dependency.product) {
        productDep = dependency.product; // We have already done the look-up.
    } else if (!(productDep = findMatchingProduct(dependency))) {
        moduleItem = findMatchingModule(dependency);
    }

    if (productDep) {
        const auto checkResult = checkProductDependency(dependency, *productDep);

        // We found a product dependency, but that product has not been handled yet,
        // so stop dependency resolving for the current product and resume it later, when the
        // dependency is ready.
        if (checkResult.second == HandleDependency::Defer)
            return {nullptr, productDep, HandleDependency::Defer};

        if (checkResult.first) {
            QBS_CHECK(productDep->mergedExportItem);
            moduleItem = productDep->mergedExportItem->clone(m_loaderState.itemPool());
            moduleItem->setParent(nullptr);

            // Needed for isolated Export item evaluation.
            moduleItem->setPrototype(productDep->mergedExportItem);
        } else {
            // The product dependency is faulty, but Depends.reqired was false.
            productDep = nullptr;
        }
    }

    if (moduleItem)
        checkModule(dependency, moduleItem, productDep);

    // Can only happen with multiplexing.
    if (moduleWithSameName && moduleWithSameName != moduleItem)
        QBS_CHECK(productDep);

    // The loading name is only used to ensure consistent sorting in case of equal
    // value priorities; see ModulePropertyMerger.
    QString loadingName;
    if (loadingItem == m_product.item) {
        loadingName = m_product.name;
    } else if (m_product.dependenciesContext && !stateStack().empty()) {
        const auto &loadingItemOrigin = stateStack().front().loadingItemOrigin;
        loadingName = loadingItemOrigin.name.toString() + loadingItemOrigin.multiplexId
                      + loadingItemOrigin.profile;
    }
    instantiateModule({m_product, loadingItem, loadingName, moduleItem, moduleWithSameName,
        productDep ? productDep->item : nullptr, dependency.name, dependency.id(),
        bool(existingModule)}, m_loaderState);

    // At this point, a null module item is only possible for a non-required dependency.
    // Note that we still needed to to the instantiation above, as that injects the module
    // name into the surrounding item for the ".present" check.
    if (!moduleItem) {
        QBS_CHECK(!dependency.requiredGlobally);
        return {nullptr, nullptr, HandleDependency::Ignore};
    }

    const auto addLocalModule = [&] {
        if (loadingItem->type() == ItemType::ModuleInstance
            && !findExistingModule(dependency, loadingItem).first) {
            loadingItem->addModule(createModule(dependency, moduleItem, productDep));
        }
    };

    // The module has already been loaded, so we don't need to add it to the product's list of
    // modules, nor do we need to handle its dependencies. The only thing we might need to
    // do is to add it to the "local" list of modules of the loading item, if it isn't in there yet.
    if (existingModule) {
        addLocalModule();
        return {nullptr, nullptr, HandleDependency::Ignore};
    }

    qCDebug(lcModuleLoader) << "module loaded:" << dependency.name.toString();
    if (m_product.item) {
        Item::Module module = createModule(dependency, moduleItem, productDep);
        module.required = dependency.requiredGlobally;
        addLoadingItem(module, *loadingItem);
        module.maxDependsChainLength = dependsChainLength();
        m_product.item->addModule(module);
        addLocalModule();
    }
    return {moduleItem, nullptr, HandleDependency::Use};
}

std::pair<Item::Module *, Item *> DependenciesResolver::findExistingModule(
    const FullyResolvedDependsItem &dependency, Item *item)
{
    if (!item) // Happens if and only if called via loadBaseModule().
        return {};
    Item *moduleWithSameName = nullptr;
    for (Item::Module &m : item->modules()) {
        if (m.name != dependency.name)
            continue;
        if (!m.product) {
            QBS_CHECK(!dependency.product);
            return {&m, m.item};
        }
        if ((dependency.profile.isEmpty() || (m.product->profileName == dependency.profile))
            && m.product->multiplexConfigurationId == dependency.multiplexId) {
            return {&m, m.item};
        }

        // This can happen if there are dependencies to several variants of a multiplexed
        // product.
        moduleWithSameName = m.item;
    }
    return {nullptr, moduleWithSameName};
}

void DependenciesResolver::updateModule(
    Item::Module &module, const FullyResolvedDependsItem &dependency)
{
    forwardParameterDeclarations(dependency.item, m_product.item->modules());
    module.versionRange.narrowDown(dependency.versionRange);
    module.required |= dependency.requiredGlobally;
    if (dependsChainLength() > module.maxDependsChainLength)
        module.maxDependsChainLength = dependsChainLength();
}

int DependenciesResolver::dependsChainLength()
{
    return m_product.dependenciesContext ? stateStack().size() : 1;
}

ProductContext *DependenciesResolver::findMatchingProduct(
    const FullyResolvedDependsItem &dependency)
{
    const auto constraint = [this, &dependency](ProductContext &product) {
        if (product.multiplexConfigurationId != dependency.multiplexId)
            return false;
        if (!dependency.profile.isEmpty() && dependency.profile != product.profileName)
            return false;
        if (dependency.limitToSubProject && !haveSameSubProject(m_product, product))
            return false;
        return true;

    };
    return m_product.project->topLevelProject->productWithNameAndConstraint(
                dependency.name.toString(), constraint);
}

Item *DependenciesResolver::findMatchingModule(
    const FullyResolvedDependsItem &dependency)
{
    // If we can tell that this is supposed to be a product dependency, we can skip
    // the module look-up.
    if (!dependency.profile.isEmpty() || !dependency.multiplexId.isEmpty()) {
        if (dependency.requiredGlobally) {
            if (!dependency.profile.isEmpty()) {
                throw ErrorInfo(Tr::tr("Product '%1' depends on '%2', which does not exist "
                                       "for the requested profile '%3'.")
                                .arg(m_product.displayName(), dependency.displayName(),
                                         dependency.profile),
                                m_product.item->location());
            }
            throw ErrorInfo(Tr::tr("Product '%1' depends on '%2', which does not exist.")
                            .arg(m_product.displayName(), dependency.displayName()),
                            m_product.item->location());
        }
        return nullptr;
    }

    if (Item *moduleItem = searchAndLoadModuleFile(m_loaderState, m_product, dependency.location(),
                                                   dependency.name, dependency.fallbackMode)) {
        QBS_CHECK(moduleItem->type() == ItemType::Module);
        Item * const proto = moduleItem;
        ModuleItemLocker locker(*moduleItem);
        moduleItem = moduleItem->clone(m_loaderState.itemPool());
        moduleItem->setPrototype(proto); // For parameter declarations.
        return moduleItem;
    }

    if (!dependency.requiredGlobally) {
        return createNonPresentModule(m_loaderState.itemPool(), dependency.name.toString(),
                                      QStringLiteral("not found"), nullptr);
    }

    throw ErrorInfo(Tr::tr("Dependency '%1' not found for product '%2'.")
                        .arg(dependency.name.toString(), m_product.displayName()),
                    dependency.location());
}

std::pair<bool, HandleDependency> DependenciesResolver::checkProductDependency(
    const FullyResolvedDependsItem &depSpec, const ProductContext &dep)
{
    // Optimization: If we already checked the product earlier and then deferred, we don't
    //               need to check it again.
    if (!depSpec.checkProduct)
        return {true, HandleDependency::Use};

    if (&dep == &m_product) {
        throw ErrorInfo(Tr::tr("Dependency '%1' refers to itself.").arg(depSpec.name.toString()),
                        depSpec.location());
    }

    if (m_product.project->topLevelProject->isProductQueuedForHandling(dep)) {
        if (m_deferral == Deferral::Allowed)
            return {false, HandleDependency::Defer};
        ErrorInfo e;
        e.append(Tr::tr("Cyclic dependencies detected:"));
        e.append(Tr::tr("First product is '%1'.")
                 .arg(m_product.displayName()), m_product.item->location());
        e.append(Tr::tr("Second product is '%1'.")
                     .arg(dep.displayName()), dep.item->location());
        e.append(Tr::tr("Requested here."), depSpec.location());
        throw e;
    }

    // This covers both the case of user-disabled products and products with errors.
    // The latter are force-disabled in ProductContext::handleError().
    if (m_product.project->topLevelProject->isDisabledItem(dep.item)) {
        if (depSpec.requiredGlobally) {
            ErrorInfo e;
            e.append(Tr::tr("Product '%1' depends on '%2',")
                     .arg(m_product.displayName(), dep.displayName()),
                     m_product.item->location());
            e.append(Tr::tr("but product '%1' is disabled.").arg(dep.displayName()),
                     dep.item->location());
            throw e;
        }
        return {false, HandleDependency::Ignore};
    }
    return {true, HandleDependency::Use};
}

void DependenciesResolver::checkModule(
    const FullyResolvedDependsItem &dependency, Item *moduleItem, ProductContext *productDep)
{
    // When loading a pseudo or temporary qbs module in early setup via loadBaseModule(),
    // there is no proper state yet.
    if (!m_product.dependenciesContext)
        return;

    for (auto it = stateStack().begin(); it != stateStack().end(); ++it) {
        Item *itemToCheck = moduleItem;
        if (it->loadingItem != itemToCheck) {
            if (!productDep)
                continue;
            itemToCheck = productDep->item;
        }
        if (it->loadingItem != itemToCheck)
            continue;
        ErrorInfo e;
        e.append(Tr::tr("Cyclic dependencies detected:"));
        while (true) {
            e.append(it->loadingItemOrigin.name.toString(),
                     it->loadingItemOrigin.location());
            if (it->loadingItem->type() == ItemType::ModuleInstance) {
                createNonPresentModule(m_loaderState.itemPool(),
                                       it->loadingItemOrigin.name.toString(),
                                       QLatin1String("cyclic dependency"), it->loadingItem);
            }
            if (it == stateStack().begin())
                break;
            --it;
        }
        e.append(dependency.name.toString(), dependency.location());
        throw e;
    }
    checkForModuleNamePrefixCollision(dependency);
}

void DependenciesResolver::adjustDependsItemForMultiplexing(Item *dependsItem)
{
    if (m_product.name.startsWith(StringConstants::shadowProductPrefix()))
        return;

    Evaluator &evaluator = m_loaderState.evaluator();
    const QString name = evaluator.stringValue(dependsItem, StringConstants::nameProperty());
    const bool productIsMultiplexed = !m_product.multiplexConfigurationId.isEmpty();
    if (name == m_product.name) {
        QBS_CHECK(!productIsMultiplexed); // This product must be an aggregator.
        return;
    }

    bool hasNonMultiplexedDependency = false;
    const std::vector<ProductContext *> multiplexedDependencies = m_product.project
            ->topLevelProject->productsWithNameAndConstraint(name, [&hasNonMultiplexedDependency]
                                                             (const ProductContext &product) {
        if (product.multiplexConfigurationId.isEmpty()) {
            hasNonMultiplexedDependency = true;
            return false;
        }
        return true;
    });
    const bool hasMultiplexedDependencies = !multiplexedDependencies.empty();

    // These are the allowed cases:
    // (1) Normal dependency with no multiplexing whatsoever.
    // (2) Both product and dependency are multiplexed.
    //     (2a) The profiles property is not set, we want to depend on the best
    //          matching variant.
    //     (2b) The profiles property is set, we want to depend on all variants
    //          with a matching profile.
    // (3) The product is not multiplexed, but the dependency is.
    //     (3a) The profiles property is not set, the dependency has an aggregator.
    //          We want to depend on the aggregator.
    //     (3b) The profiles property is not set, the dependency does not have an
    //          aggregator. We want to depend on all the multiplexed variants.
    //     (3c) The profiles property is set, we want to depend on all variants
    //          with a matching profile regardless of whether an aggregator exists or not.
    // (4) The product is multiplexed, but the dependency is not. We don't have to adapt
    //     any Depends items.
    // (1) and (4)
    if (!hasMultiplexedDependencies)
        return;

    bool profilesPropertyIsSet;
    const QStringList profiles = evaluator.stringListValue(
        dependsItem, StringConstants::profilesProperty(), &profilesPropertyIsSet);

    // (3a)
    if (!productIsMultiplexed && hasNonMultiplexedDependency && !profilesPropertyIsSet)
        return;

    static const auto multiplexConfigurationIntersects = [](const QVariantMap &lhs,
                                                            const QVariantMap &rhs) {
        QBS_CHECK(!lhs.isEmpty() && !rhs.isEmpty());
        for (auto lhsProperty = lhs.constBegin(); lhsProperty != lhs.constEnd(); lhsProperty++) {
            const auto rhsProperty = rhs.find(lhsProperty.key());
            const bool isCommonProperty = rhsProperty != rhs.constEnd();
            if (isCommonProperty && lhsProperty.value() != rhsProperty.value())
                return false;
        }
        return true;
    };

    QStringList multiplexIds;
    const auto productMultiplexConfig = m_loaderState.topLevelProject().multiplexConfiguration(
                m_product.multiplexConfigurationId);

    for (const ProductContext *dependency : multiplexedDependencies) {
        if (productIsMultiplexed && !profilesPropertyIsSet) { // 2a
            if (dependency->multiplexConfigurationId == m_product.multiplexConfigurationId) {
                const ValuePtr &multiplexId = m_product.item->property(
                    StringConstants::multiplexConfigurationIdProperty());
                dependsItem->setProperty(StringConstants::multiplexConfigurationIdsProperty(),
                                         multiplexId);
                return;

            }
            // Otherwise collect partial matches and decide later
            const auto dependencyMultiplexConfig = m_loaderState.topLevelProject()
                    .multiplexConfiguration(dependency->multiplexConfigurationId);

            if (multiplexConfigurationIntersects(dependencyMultiplexConfig, productMultiplexConfig))
                multiplexIds << dependency->multiplexConfigurationId;
        } else {
            // (2b), (3b) or (3c)
            const bool profileMatch = !profilesPropertyIsSet || profiles.empty()
                                      || profiles.contains(dependency->profileName);
            if (profileMatch)
                multiplexIds << dependency->multiplexConfigurationId;
        }
    }
    if (multiplexIds.empty()) {
        const QString productName = m_product.displayName();
        throw ErrorInfo(Tr::tr("Dependency from product '%1' to product '%2' not fulfilled. "
                               "There are no eligible multiplex candidates.").arg(productName,
                                 name),
                        dependsItem->location());
    }

    // In case of (2a), at most 1 match is allowed
    if (productIsMultiplexed && !profilesPropertyIsSet && multiplexIds.size() > 1) {
        QStringList candidateNames;
        for (const auto &id : std::as_const(multiplexIds))
            candidateNames << fullProductDisplayName(name, id);
        throw ErrorInfo(
            Tr::tr("Dependency from product '%1' to product '%2' is ambiguous. "
                   "Eligible multiplex candidates: %3.").arg(
                        m_product.displayName(), name, candidateNames.join(QLatin1String(", "))),
            dependsItem->location());
    }

    dependsItem->setProperty(StringConstants::multiplexConfigurationIdsProperty(),
                             VariantValue::create(multiplexIds));

}

std::optional<EvaluatedDependsItem> DependenciesResolver::evaluateDependsItem(Item *item)
{
    Evaluator &evaluator = m_loaderState.evaluator();
    if (!m_product.project->topLevelProject->checkItemCondition(item, evaluator)) {
        qCDebug(lcModuleLoader) << "Depends item disabled, ignoring.";
        return {};
    }

    const QString name = evaluator.stringValue(item, StringConstants::nameProperty());
    if (name == StringConstants::qbsModule()) // Redundant
        return {};

    bool submodulesPropertySet;
    const QStringList submodules = evaluator.stringListValue(
        item, StringConstants::submodulesProperty(), &submodulesPropertySet);
    if (submodules.empty() && submodulesPropertySet) {
        qCDebug(lcModuleLoader) << "Ignoring Depends item with empty submodules list.";
        return {};
    }
    if (Q_UNLIKELY(submodules.size() > 1 && !item->id().isEmpty())) {
        QString msg = Tr::tr("A Depends item with more than one module cannot have an id.");
        throw ErrorInfo(msg, item->location());
    }
    bool productTypesWasSet;
    const QStringList productTypes = evaluator.stringListValue(
        item, StringConstants::productTypesProperty(), &productTypesWasSet);
    if (!name.isEmpty() && !productTypes.isEmpty()) {
        throw ErrorInfo(Tr::tr("The name and productTypes are mutually exclusive "
                               "in a Depends item."), item->location());
    }
    if (productTypes.isEmpty() && productTypesWasSet) {
        qCDebug(lcModuleLoader) << "Ignoring Depends item with empty productTypes list.";
        return {};
    }
    if (name.isEmpty() && !productTypesWasSet) {
        throw ErrorInfo(Tr::tr("Either name or productTypes must be set in a Depends item"),
                        item->location());
    }

    const FallbackMode fallbackMode
            = m_loaderState.parameters().fallbackProviderEnabled()
                  && evaluator.boolValue(item, StringConstants::enableFallbackProperty())
              ? FallbackMode::Enabled : FallbackMode::Disabled;

    bool profilesPropertyWasSet = false;
    std::optional<QStringList> profiles;
    bool required = true;
    if (productTypes.isEmpty()) {
        const QStringList profileList = evaluator.stringListValue(
            item, StringConstants::profilesProperty(), &profilesPropertyWasSet);
        if (profilesPropertyWasSet)
            profiles.emplace(profileList);
        required = evaluator.boolValue(item, StringConstants::requiredProperty());
    }
    const Version minVersion = Version::fromString(
        evaluator.stringValue(item, StringConstants::versionAtLeastProperty()));
    const Version maxVersion = Version::fromString(
        evaluator.stringValue(item, StringConstants::versionBelowProperty()));
    const bool limitToSubProject = evaluator.boolValue(
        item, StringConstants::limitToSubProjectProperty());
    const QStringList multiplexIds = evaluator.stringListValue(
        item, StringConstants::multiplexConfigurationIdsProperty());
    adjustParametersScopes(item, item);
    forwardParameterDeclarations(item, m_product.item->modules());
    const QVariantMap parameters = extractParameters(item);

    const FileTags productTypeTags = FileTags::fromStringList(productTypes);
    if (!productTypeTags.empty())
        m_product.bulkDependencies.emplace_back(productTypeTags, item->location());
    return EvaluatedDependsItem{
        item, QualifiedId::fromString(name), submodules, productTypeTags,
        multiplexIds, profiles, {minVersion, maxVersion}, parameters, limitToSubProject,
        fallbackMode, required};
}

// Potentially multiplexes a dependency along Depends.productTypes, Depends.subModules and
// Depends.profiles, as well as internally set up multiplexing axes.
// Each entry in the resulting queue corresponds to exactly one product or module to pull in.
std::queue<FullyResolvedDependsItem>
DependenciesResolver::multiplexDependency(const EvaluatedDependsItem &dependency)
{
    std::queue<FullyResolvedDependsItem> dependencies;
    if (!dependency.productTypes.empty()) {
        const auto constraint = [&](const ProductContext &product) {
            return &product != &m_product && product.name != m_product.name
                    && (!dependency.limitToSubProject || haveSameSubProject(m_product, product));
        };
        const std::vector<ProductContext *> matchingProducts = m_product.project->topLevelProject
                ->productsWithTypeAndConstraint(dependency.productTypes, constraint);
        if (matchingProducts.empty()) {
            qCDebug(lcModuleLoader) << "Depends.productTypes does not match anything."
                                    << dependency.item->location();
            return {};
        }
        for (ProductContext * const match : matchingProducts)
            dependencies.emplace(match, dependency);
        return dependencies;
    }

    const QStringList profiles = dependency.profiles && !dependency.profiles->isEmpty()
                                     ? *dependency.profiles : QStringList(QString());
    const QStringList multiplexIds = !dependency.multiplexIds.isEmpty()
                                         ? dependency.multiplexIds : QStringList(QString());
    const QStringList subModules = !dependency.subModules.isEmpty()
                                       ? dependency.subModules : QStringList(QString());
    for (const QString &profile : profiles) {
        for (const QString &multiplexId : multiplexIds) {
            for (const QString &subModule : subModules) {
                QualifiedId name = dependency.name;
                if (!subModule.isEmpty())
                    name << subModule.split(QLatin1Char('.'));
                dependencies.emplace(dependency, name, profile, multiplexId);
            }
        }
    }
    return dependencies;
}

QVariantMap DependenciesResolver::extractParameters(Item *dependsItem) const
{
    try {
        QVariantMap result;
        const auto &properties = dependsItem->properties();
        Evaluator &evaluator = m_loaderState.evaluator();
        for (auto it = properties.begin(); it != properties.end(); ++it) {
            if (it.value()->type() != Value::ItemValueType)
                continue;
            const JSValue sv = evaluator.scriptValue(
                        std::static_pointer_cast<ItemValue>(it.value())->item());
            result.insert(it.key(), safeToVariant(evaluator.engine()->context(), sv));
        }
        return result;
    } catch (const ErrorInfo &exception) {
        auto ei = exception;
        ei.prepend(Tr::tr("Error in dependency parameter."), dependsItem->location());
        throw ei;
    }
}

void DependenciesResolver::forwardParameterDeclarations(const Item *dependsItem,
                                                            const Item::Modules &modules)
{
    for (auto it = dependsItem->properties().begin(); it != dependsItem->properties().end(); ++it) {
        if (it.value()->type() != Value::ItemValueType)
            continue;
        forwardParameterDeclarations(it.key(),
                                     std::static_pointer_cast<ItemValue>(it.value())->item(),
                                     modules);
    }
}

void DependenciesResolver::forwardParameterDeclarations(
        const QualifiedId &moduleName, Item *item, const Item::Modules &modules)
{
    auto it = std::find_if(modules.begin(), modules.end(), [&moduleName] (const Item::Module &m) {
        return m.name == moduleName;
    });
    if (it != modules.end()) {
        item->setPropertyDeclarations(m_loaderState.topLevelProject().parameterDeclarations(
                                          it->item->rootPrototype()));
    } else {
        for (auto it = item->properties().begin(); it != item->properties().end(); ++it) {
            if (it.value()->type() != Value::ItemValueType)
                continue;
            forwardParameterDeclarations(QualifiedId(moduleName) << it.key(),
                                         std::static_pointer_cast<ItemValue>(it.value())->item(),
                                         modules);
        }
    }
}

std::list<DependenciesResolvingState> &DependenciesResolver::stateStack()
{
    QBS_CHECK(m_product.dependenciesContext);
    return static_cast<DependenciesContextImpl *>(m_product.dependenciesContext.get())->stateStack;
}

void DependenciesResolver::checkForModuleNamePrefixCollision(
    const FullyResolvedDependsItem &dependency)
{
    if (!m_product.item)
        return;

    for (const Item::Module &m : m_product.item->modules()) {
        if (m.name.length() == dependency.name.length()
            || m.name.front() != dependency.name.front()) {
            continue;
        }
        QualifiedId shortName;
        QualifiedId longName;
        if (m.name < dependency.name) {
            shortName = m.name;
            longName = dependency.name;
        } else {
            shortName = dependency.name;
            longName = m.name;
        }
        throw ErrorInfo(Tr::tr("The name of module '%1' is equal to the first component of the "
                               "name of module '%2', which is not allowed")
                            .arg(shortName.toString(), longName.toString()), dependency.location());
    }
}

Item::Module DependenciesResolver::createModule(
    const FullyResolvedDependsItem &dependency, Item *item, ProductContext *productDep)
{
    Item::Module m;
    m.item = item;
    m.product = productDep;
    m.name = dependency.name;
    m.required = dependency.requiredLocally;
    m.versionRange = dependency.versionRange;
    return m;
}

FullyResolvedDependsItem::FullyResolvedDependsItem(
    ProductContext *product, const EvaluatedDependsItem &dependency)
    : product(product), item(dependency.item), name(product->name),
    versionRange(dependency.versionRange), parameters(dependency.parameters),
    fallbackMode(FallbackMode::Disabled), checkProduct(false) {}

FullyResolvedDependsItem FullyResolvedDependsItem::makeBaseDependency()
{
    FullyResolvedDependsItem item;
    item.fallbackMode = FallbackMode::Disabled;
    item.name = StringConstants::qbsModule();
    return item;
}

FullyResolvedDependsItem::FullyResolvedDependsItem(
    const EvaluatedDependsItem &dependency, QualifiedId name, QString profile, QString multiplexId)
    : item(dependency.item), name(std::move(name)),
      profile(std::move(profile)), multiplexId(std::move(multiplexId)),
      versionRange(dependency.versionRange),
      parameters(dependency.parameters),
      limitToSubProject(dependency.limitToSubProject),
      fallbackMode(dependency.fallbackMode),
      requiredLocally(dependency.requiredLocally),
      requiredGlobally(dependency.requiredGlobally) {}

QString FullyResolvedDependsItem::id() const
{
    if (!item) {
        QBS_CHECK(name.toString() == StringConstants::qbsModule());
        return {};
    }
    return item->id();
}

CodeLocation FullyResolvedDependsItem::location() const
{
    if (!item) {
        QBS_CHECK(name.toString() == StringConstants::qbsModule());
        return {};
    }
    return item->location();
}

QString FullyResolvedDependsItem::displayName() const
{
    return fullProductDisplayName(name.toString(), multiplexId);
}

bool haveSameSubProject(const ProductContext &p1, const ProductContext &p2)
{
    for (const Item *otherParent = p2.item->parent(); otherParent;
         otherParent = otherParent->parent()) {
        if (otherParent == p1.item->parent())
            return true;
    }
    return false;
}

QVariantMap safeToVariant(JSContext *ctx, const JSValue &v)
{
    QVariantMap result;
    handleJsProperties(ctx, v, [&](const JSAtom &prop, const JSPropertyDescriptor &desc) {
        const JSValue u = desc.value;
        if (JS_IsError(ctx, u))
            throw ErrorInfo(getJsString(ctx, u));
        const QString name = getJsString(ctx, prop);
        result[name] = (JS_IsObject(u) && !JS_IsArray(ctx, u) && !JS_IsRegExp(ctx, u))
                           ? safeToVariant(ctx, u) : getJsVariant(ctx, u);
    });
    return result;
}

DependenciesContextImpl::DependenciesContextImpl(ProductContext &product, LoaderState &loaderState)
    : m_product(product)
{
    setSearchPathsForProduct(loaderState);

    // Initialize the state with the direct Depends items of the product item.
    DependenciesResolvingState newState{product.item,};
    for (Item * const child : product.item->children()) {
        if (child->type() == ItemType::Depends)
            newState.pendingDependsItems.push(child);
    }
    stateStack.push_front(std::move(newState));
    stateStack.front().pendingResolvedDependencies.push(
        FullyResolvedDependsItem::makeBaseDependency());
}

std::pair<ProductDependency, ProductContext *> DependenciesContextImpl::pendingDependency() const
{
    QBS_CHECK(!stateStack.empty());
    if (stateStack.front().currentDependsItem
        && !stateStack.front().currentDependsItem->productTypes.empty()) {
        qCDebug(lcLoaderScheduling) << "product" << m_product.displayName()
                                    << "to be delayed because of bulk dependency";
        return {ProductDependency::Bulk, nullptr};
    }
    if (!stateStack.front().pendingResolvedDependencies.empty()) {
        if (ProductContext * const dep = stateStack.front().pendingResolvedDependencies
                .front().product) {
            if (m_product.project->topLevelProject->isProductQueuedForHandling(*dep)) {
                qCDebug(lcLoaderScheduling) << "product" << m_product.displayName()
                                            << "to be delayed because of dependency "
                                               "to unfinished product" << dep->displayName();
                return {ProductDependency::Single, dep};
            } else {
                qCDebug(lcLoaderScheduling) << "product" << m_product.displayName()
                                            << "to be re-scheduled, as dependency "
                                            << dep->displayName()
                                            << "appears to have finished in the meantime";
                return {ProductDependency::None, dep};
            }
        }
    }
    return {ProductDependency::None, nullptr};
}

void DependenciesContextImpl::setSearchPathsForProduct(LoaderState &loaderState)
{
    QBS_CHECK(m_product.searchPaths.isEmpty());

    m_product.searchPaths = loaderState.itemReader().readExtraSearchPaths(m_product.item);
    Settings settings(loaderState.parameters().settingsDirectory());
    const QStringList prefsSearchPaths = Preferences(&settings, m_product.profileModuleProperties)
            .searchPaths();
    const QStringList &currentSearchPaths = loaderState.itemReader().allSearchPaths();
    for (const QString &p : prefsSearchPaths) {
        if (!currentSearchPaths.contains(p) && FileInfo(p).exists())
            m_product.searchPaths << p;
    }
}

} // namespace
} // namespace qbs::Internal
