/**************************************************************************
**
** Copyright (C) 2023 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Installer Framework.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** 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 General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** 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-3.0.html.
**
** $QT_END_LICENSE$
**
**************************************************************************/

#include "installercalculator.h"

#include "component.h"
#include "packagemanagercore.h"
#include "settings.h"
#include <globals.h>

namespace QInstaller {

/*!
    \inmodule QtInstallerFramework
    \class QInstaller::InstallerCalculator
    \internal
*/

InstallerCalculator::InstallerCalculator(PackageManagerCore *core, const AutoDependencyHash &autoDependencyComponentHash)
    : CalculatorBase(core)
    , m_autoDependencyComponentHash(autoDependencyComponentHash)
{
}

InstallerCalculator::~InstallerCalculator()
{
}

QString InstallerCalculator::resolutionText(Component *component) const
{
    const Resolution reason = resolutionType(component);
    switch (reason) {
        case Resolution::Automatic:
            return QCoreApplication::translate("InstallerCalculator",
                "Components added as automatic dependencies:");
        case Resolution::Dependent:
            return QCoreApplication::translate("InstallerCalculator", "Components added as "
                "dependency for \"%1\":").arg(referencedComponent(component));
        case Resolution::Resolved:
            return QCoreApplication::translate("InstallerCalculator",
                "Components that have resolved dependencies:");
        case Resolution::Selected:
            return QCoreApplication::translate("InstallerCalculator",
                "Selected components without dependencies:");
        default:
            Q_ASSERT_X(false, Q_FUNC_INFO, "Invalid install resolution detected!");
    }
    return QString();
}

bool InstallerCalculator::solve(const QList<Component *> &components)
{
    if (components.isEmpty())
        return true;

    QList<Component*> notAppendedComponents; // for example components with unresolved dependencies
    for (Component *component : qAsConst(components)){
        if (!component)
            continue;
        if (m_toInstallComponentIds.contains(component->name())) {
            const QString errorMessage = recursionError(component);
            qCWarning(QInstaller::lcInstallerInstallLog).noquote() << errorMessage;
            m_errorString.append(errorMessage);
            Q_ASSERT_X(!m_toInstallComponentIds.contains(component->name()), Q_FUNC_INFO,
                qPrintable(errorMessage));
            return false;
        }

        if (component->currentDependencies().isEmpty())
            addComponentForInstall(component);
        else
            notAppendedComponents.append(component);
    }

    for (Component *component : qAsConst(notAppendedComponents)) {
        if (!solveComponent(component))
            return false;
    }

    // All regular dependencies are resolved. Now we are looking for auto depend on components.
    QSet<Component *> foundAutoDependOnList = autodependencyComponents();
    if (!foundAutoDependOnList.isEmpty())
        return solve(foundAutoDependOnList.values());

    return true;
}

void InstallerCalculator::addComponentForInstall(Component *component, const QString &version)
{
    if (!m_componentsForAutodepencencyCheck.contains(component))
        m_componentsForAutodepencencyCheck.append(component);

    if (!component->isInstalled(version) || (m_core->isUpdater() && component->isUpdateAvailable())) {
        m_resolvedComponents.append(component);
        m_toInstallComponentIds.insert(component->name());
    }
}

QString InstallerCalculator::recursionError(Component *component) const
{
    return QCoreApplication::translate("InstallerCalculator", "Recursion detected, component \"%1\" "
        "already added with reason: \"%2\"").arg(component->name(), resolutionText(component));
}

bool InstallerCalculator::solveComponent(Component *component, const QString &version)
{
    const QStringList dependenciesList = component->currentDependencies();
    QString requiredDependencyVersion = version;
    for (const QString &dependencyComponentName : dependenciesList) {
        // PackageManagerCore::componentByName returns 0 if dependencyComponentName contains a
        // version which is not available
        Component *dependencyComponent = m_core->componentByName(dependencyComponentName);
        if (!dependencyComponent) {
            const QString errorMessage = QCoreApplication::translate("InstallerCalculator",
                "Cannot find missing dependency \"%1\" for \"%2\".").arg(dependencyComponentName,
                component->name());
            qCWarning(QInstaller::lcInstallerInstallLog).noquote() << errorMessage;
            m_errorString.append(errorMessage);
            if (component->packageManagerCore()->settings().allowUnstableComponents()) {
                component->setUnstable(Component::UnstableError::MissingDependency, errorMessage);
                continue;
            } else {
                return false;
            }
        }
        //Check if component requires higher version than what might be already installed
        bool isUpdateRequired = false;
        QString requiredName;
        QString requiredVersion;
        PackageManagerCore::parseNameAndVersion(dependencyComponentName, &requiredName, &requiredVersion);
        if (!requiredVersion.isEmpty() &&
                !dependencyComponent->value(scInstalledVersion).isEmpty()) {
            static const QRegularExpression compEx(QLatin1String("^([<=>]+)(.*)$"));
            QRegularExpressionMatch match = compEx.match(dependencyComponent->value(scInstalledVersion));
            const QString installedVersion = match.hasMatch()
                    ? match.captured(2) : dependencyComponent->value(scInstalledVersion);

            match = compEx.match(requiredVersion);
            requiredVersion = match.hasMatch() ? match.captured(2) : requiredVersion;

            if (KDUpdater::compareVersion(requiredVersion, installedVersion) >= 1 ) {
                isUpdateRequired = true;
                requiredDependencyVersion = requiredVersion;
            }
        }
        //Check dependencies only if
        //- Dependency is not installed or update requested, nor newer version of dependency component required
        //- And dependency component is not already added for install
        //- And component is not already added for install, then dependencies are already resolved
        if (((!dependencyComponent->isInstalled() || dependencyComponent->updateRequested())
                || isUpdateRequired) && (!m_toInstallComponentIds.contains(dependencyComponent->name())
                && !m_toInstallComponentIds.contains(component->name()))) {
            if (m_visitedComponents.value(component).contains(dependencyComponent)) {
                const QString errorMessage = recursionError(component);
                qCWarning(QInstaller::lcInstallerInstallLog).noquote() << errorMessage;
                m_errorString = errorMessage;
                Q_ASSERT_X(!m_visitedComponents.value(component).contains(dependencyComponent),
                    Q_FUNC_INFO, qPrintable(errorMessage));
                return false;
            }
            m_visitedComponents[component].insert(dependencyComponent);
            // add needed dependency components to the next run
            insertResolution(dependencyComponent, Resolution::Dependent,
                component->name());

            if (!solveComponent(dependencyComponent, requiredDependencyVersion))
                return false;
        }
    }

    if (!m_toInstallComponentIds.contains(component->name())) {
        addComponentForInstall(component, requiredDependencyVersion);
        insertResolution(component, Resolution::Resolved);
    }
    return true;
}

QSet<Component *> InstallerCalculator::autodependencyComponents()
{
    // All regular dependencies are resolved. Now we are looking for auto depend on components.
    // m_componentsForAutodepencencyCheck is a list of recently calculated components to be installed
    // (normal components and regular dependencies components), and we check possible installable auto
    // dependency components based on that list.
    QSet<Component *> foundAutoDependOnList;
    for (const Component *component : qAsConst(m_componentsForAutodepencencyCheck)) {
        if (!m_autoDependencyComponentHash.contains(component->name())
                || (m_core->isUpdater() && !component->updateRequested()))
            continue;
        for (const QString& autoDependency : m_autoDependencyComponentHash.value(component->name())) {
            // If a components is already installed or is scheduled for installation, no need to check
            // for auto depend installation.
            if (m_toInstallComponentIds.contains(autoDependency)) {
                continue;
            }
            Component *autoDependComponent = m_core->componentByName(autoDependency);
            if (!autoDependComponent)
                continue;
            if ((!autoDependComponent->isInstalled()
                 || (m_core->isUpdater() && autoDependComponent->isUpdateAvailable()))
                && !m_toInstallComponentIds.contains(autoDependComponent->name())) {
                // One of the components autodependons is requested for install, check if there
                // are other autodependencies as well
                if (autoDependComponent->isAutoDependOn(m_toInstallComponentIds)) {
                    foundAutoDependOnList.insert(autoDependComponent);
                    insertResolution(autoDependComponent, Resolution::Automatic);
                }
            }
        }
    }
    m_componentsForAutodepencencyCheck.clear();
    return foundAutoDependOnList;
}

} // namespace QInstaller
