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

#include "kitaspect.h"

#include "devicesupport/devicekitaspects.h"
#include "devicesupport/idevice.h"
#include "kit.h"
#include "kitmanager.h"
#include "projectexplorertr.h"

#include <coreplugin/icore.h>

#include <utils/algorithm.h>
#include <utils/environment.h>
#include <utils/guard.h>
#include <utils/guiutils.h>
#include <utils/layoutbuilder.h>
#include <utils/treemodel.h>

#include <QAction>
#include <QComboBox>
#include <QLabel>
#include <QPushButton>

#include <utility>

using namespace Core;
using namespace Utils;

static const char DETECTIONSOURCETYPE[] = "DetectionSource.type";
static const char DETECTIONSOURCEID[] = "DetectionSource.id";

namespace ProjectExplorer {

void DetectionSource::fromMap(const Store &store)
{
    type = static_cast<DetectionType>(store.value(DETECTIONSOURCETYPE).toInt());
    id = store.value(DETECTIONSOURCEID).toString();
}

void DetectionSource::toMap(Store &store) const
{
    store.insert(DETECTIONSOURCETYPE, static_cast<int>(type));
    store.insert(DETECTIONSOURCEID, id);
}

std::optional<DetectionSource> DetectionSource::createFromMap(const Store &store)
{
    if (store.contains(DETECTIONSOURCETYPE) && store.contains(DETECTIONSOURCEID)) {
        DetectionSource ds;
        ds.fromMap(store);
        return ds;
    }
    return {};
}

namespace {

class KitAspectComboBox final : public QComboBox
{
public:
    using QComboBox::QComboBox;

    bool event(QEvent *e) final
    {
        if (e->type() == QEvent::ToolTip)
            setToolTip(itemData(currentIndex(), Qt::ToolTipRole).toString());
        return QComboBox::event(e);
    }
};

class KitAspectSortModel final : public SortModel
{
public:
    using SortModel::SortModel;

private:
    bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override
    {
        const auto getValue = [&](const QModelIndex &index, KitAspect::ItemRole role) {
            return sourceModel()->data(index, role);
        };
        const auto getValues = [&]<typename T>(KitAspect::ItemRole role, const T & /* dummy */) {
            return std::make_pair(
                sourceModel()->data(source_left, role).value<T>(),
                sourceModel()->data(source_right, role).value<T>());
        };

        // Criterion 1: "None" item come last.
        if (getValue(source_left, KitAspect::IsNoneRole).toBool())
            return false;

        if (getValue(source_right, KitAspect::IsNoneRole).toBool())
            return true;

        // Criterion 2: "Type", which is is the name of some category by which the entries
        //              are supposed to get grouped together.
        if (const auto [type1, type2] = getValues(KitAspect::TypeRole, QString()); type1 != type2)
            return type1 < type2;

        // Criterion 3: "Quality", i.e. how likely is the respective entry to be usable.
        if (const auto [qual1, qual2] = getValues(KitAspect::QualityRole, int()); qual1 != qual2)
            return qual1 > qual2;

        // Criterion 4: Name.
        return SortModel::lessThan(source_left, source_right);
    }
};

class KitAspectFactories
{
public:
    void onKitsLoaded() const
    {
        for (KitAspectFactory *factory : m_aspectList)
            factory->onKitsLoaded();
    }

    void addKitAspect(KitAspectFactory *factory)
    {
        QTC_ASSERT(!m_aspectList.contains(factory), return);
        m_aspectList.append(factory);
        m_aspectListIsSorted = false;
    }

    void removeKitAspect(KitAspectFactory *factory)
    {
        int removed = m_aspectList.removeAll(factory);
        QTC_CHECK(removed == 1);
    }

    const QList<KitAspectFactory *> kitAspectFactories()
    {
        if (!m_aspectListIsSorted) {
            Utils::sort(m_aspectList, [](const KitAspectFactory *a, const KitAspectFactory *b) {
                return a->priority() > b->priority();
            });
            m_aspectListIsSorted = true;
        }
        return m_aspectList;
    }

    // Sorted by priority, in descending order...
    QList<KitAspectFactory *> m_aspectList;
    // ... if this here is set:
    bool m_aspectListIsSorted = true;
};

static KitAspectFactories &kitAspectFactoriesStorage()
{
    static KitAspectFactories theKitAspectFactories;
    return theKitAspectFactories;
}

} // namespace

class KitAspect::Private
{
public:
    Private(Kit *kit, const KitAspectFactory *f)
        : kit(kit)
        , factory(f)
    {}

    Kit *kit;
    const KitAspectFactory * const factory;
    QAction *mutableAction = nullptr;
    Id managingPageId;
    QPushButton *manageButton = nullptr;
    Guard ignoreChanges;
    QList<KitAspect *> aspectsToEmbed;

    struct ListAspect
    {
        ListAspect(const ListAspectSpec &spec, QComboBox *comboBox)
            : spec(spec)
            , comboBox(comboBox)
        {}
        ListAspectSpec spec;
        QComboBox *comboBox;
    };
    QList<ListAspect> listAspects;

    bool readOnly = false;
};

KitAspect::KitAspect(Kit *k, const KitAspectFactory *factory)
    : d(new Private(k, factory))
{
    connect(KitManager::instance(), &KitManager::kitRemoved, this, [this](Kit *k) {
        if (k == d->kit)
            d->kit = nullptr;
    });

    const Id id = factory->id();
    d->mutableAction = new QAction(Tr::tr("Mark as Mutable"));
    d->mutableAction->setCheckable(true);
    d->mutableAction->setChecked(k->isMutable(id));
    d->mutableAction->setEnabled(!k->isSticky(id));
    connect(d->mutableAction, &QAction::toggled, this, [this, id] {
        if (Kit *k = kit())
            k->setMutable(id, d->mutableAction->isChecked());
    });
}

KitAspect::~KitAspect()
{
    delete d->mutableAction;
    delete d;
}

void KitAspect::refresh()
{
    if (d->listAspects.isEmpty() || d->ignoreChanges.isLocked())
        return;
    const GuardLocker locker(d->ignoreChanges);

    Kit *k = kit();
    if (!k)
        return;

    for (const Private::ListAspect &la : std::as_const(d->listAspects)) {
        // Prevent dirtying by the QComboBox::model()::reset and setCurrentIndex below.
        DirtySettingsGuard guard;

        la.spec.resetModel();
        la.comboBox->model()->sort(0);
        const QVariant itemId = la.spec.getter(*k);
        int idx = la.comboBox->findData(itemId, IdRole);
        if (idx == -1) {
            idx = la.comboBox->count() - 1;
            if (QTC_UNEXPECTED(itemId.isValid())) {
                qWarning() << factory()->displayName();
                const QVariant newId = idx < 0 ? QVariant() : la.comboBox->itemData(idx, IdRole);
                la.spec.setter(*kit(), newId);
            }
        }
        la.comboBox->setCurrentIndex(idx);
        la.comboBox->setEnabled(!d->readOnly && la.comboBox->count() > 1);
    }
}

void KitAspect::makeStickySubWidgetsReadOnly()
{
    auto kit = this->kit();
    QTC_ASSERT(kit, return);

    if (!kit->isSticky(d->factory->id()))
        return;

    if (d->manageButton)
        d->manageButton->setEnabled(false);

    d->readOnly = true;
    makeReadOnly(true);
}

void KitAspect::reload()
{
    if (d->readOnly) {
        d->readOnly = false;
        if (d->manageButton)
            d->manageButton->setEnabled(true);
        makeReadOnly(false);
    }
    const Id id = factory()->id();
    if (Kit *k = kit()) {
        d->mutableAction->setChecked(k->isMutable(id));
        d->mutableAction->setEnabled(!k->isSticky(id));
    }
    valueToVolatileValue();
    refresh();
}

void KitAspect::makeReadOnly(bool readOnly)
{
    for (const Private::ListAspect &la : std::as_const(d->listAspects))
        la.comboBox->setEnabled(!readOnly);
}

void KitAspect::addToInnerLayout(Layouting::Layout &layout)
{
    addListAspectsToLayout(layout);
}

void KitAspect::addListAspectSpec(const ListAspectSpec &listAspectSpec)
{
    const auto comboBox = createSubWidget<KitAspectComboBox>();
    const auto sortModel = new KitAspectSortModel(this);
    sortModel->setSourceModel(listAspectSpec.model);
    comboBox->setModel(sortModel);
    comboBox->setMinimumContentsLength(15);
    comboBox->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
    d->listAspects.emplaceBack(listAspectSpec, comboBox);

    refresh();

    connect(comboBox, &QComboBox::currentIndexChanged,
        this, [this, listAspectSpec, comboBox] {
            if (d->ignoreChanges.isLocked())
                return;

            if (Kit *k = kit())
                listAspectSpec.setter(*k, comboBox->itemData(comboBox->currentIndex(), IdRole));
        });
    connect(listAspectSpec.model, &QAbstractItemModel::modelAboutToBeReset,
            this, [this] { d->ignoreChanges.lock(); });
    connect(listAspectSpec.model, &QAbstractItemModel::modelReset,
            this, [this] { d->ignoreChanges.unlock(); });
}

QList<QComboBox *> KitAspect::comboBoxes() const
{
    return Utils::transform(d->listAspects, &Private::ListAspect::comboBox);
}

void KitAspect::addLabelToLayout(Layouting::Layout &layout)
{
    auto label = createSubWidget<QLabel>(d->factory->displayName() + ':');
    label->setToolTip(d->factory->description());
    connect(label, &QLabel::linkActivated, this, [this](const QString &link) {
        emit labelLinkActivated(link);
    });

    layout.addItem(label);
}

void KitAspect::addListAspectsToLayout(Layouting::Layout &layout)
{
    for (const Private::ListAspect &la : std::as_const(d->listAspects)) {
        addMutableAction(la.comboBox);
        layout.addItem(la.comboBox);
    }
}

void KitAspect::addManageButtonToLayout(Layouting::Layout &layout)
{
    if (d->managingPageId.isValid()) {
        d->manageButton = createSubWidget<QPushButton>(msgManage());
        setIgnoreForDirtyHook(d->manageButton);
        connect(d->manageButton, &QPushButton::clicked, this, [this] {
            Core::ICore::showSettings(d->managingPageId, settingsPageItemToPreselect());
        });
        layout.addItem(d->manageButton);
    }
}

void KitAspect::addToLayoutImpl(Layouting::Layout &layout)
{
    addLabelToLayout(layout);
    addToInnerLayout(layout);
    addManageButtonToLayout(layout);

    layout.flush();
}

void KitAspect::addMutableAction(QWidget *child)
{
    QTC_ASSERT(child, return);
    if (factory()->id() == RunDeviceKitAspect::id())
        return;
    child->addAction(d->mutableAction);
    child->setContextMenuPolicy(Qt::ActionsContextMenu);
}

void KitAspect::setManagingPage(Id pageId)
{
    d->managingPageId = pageId;
}

void KitAspect::setAspectsToEmbed(const QList<KitAspect *> &aspects)
{
    d->aspectsToEmbed = aspects;
}

QList<KitAspect *> KitAspect::aspectsToEmbed() const
{
    return d->aspectsToEmbed;
}

QString KitAspect::msgManage()
{
    return Tr::tr("Manage...");
}

Kit *KitAspect::kit() const
{
    return d->kit;
}

const KitAspectFactory *KitAspect::factory() const
{
    return d->factory;
}

QAction *KitAspect::mutableAction() const
{
    return d->mutableAction;
}

KitAspectFactory::KitAspectFactory()
{
    kitAspectFactoriesStorage().addKitAspect(this);
}

KitAspectFactory::~KitAspectFactory()
{
    kitAspectFactoriesStorage().removeKitAspect(this);
}

int KitAspectFactory::weight(const Kit *k) const
{
    return k->value(id()).isValid() ? 1 : 0;
}

QVariant KitAspectFactory::getInfo(const Kit *k, Id request, const QVariant &input) const
{
    Q_UNUSED(k)
    Q_UNUSED(request)
    Q_UNUSED(input)
    return {};
}

void KitAspectFactory::addToBuildEnvironment(const Kit *k, Environment &env) const
{
    Q_UNUSED(k)
    Q_UNUSED(env)
}

void KitAspectFactory::addToRunEnvironment(const Kit *k, Environment &env) const
{
    Q_UNUSED(k)
    Q_UNUSED(env)
}

QList<OutputLineParser *> KitAspectFactory::createOutputParsers(const Kit *k) const
{
    Q_UNUSED(k)
    return {};
}

QSet<Id> KitAspectFactory::supportedPlatforms(const Kit *k) const
{
    Q_UNUSED(k)
    return {};
}

QSet<Id> KitAspectFactory::availableFeatures(const Kit *k) const
{
    Q_UNUSED(k)
    return {};
}

void KitAspectFactory::addToMacroExpander(Kit *k, MacroExpander *expander) const
{
    Q_UNUSED(k)
    Q_UNUSED(expander)
}

void KitAspectFactory::notifyAboutUpdate(Kit *k)
{
    if (k)
        k->kitUpdated();
}

void KitAspectFactory::handleKitsLoaded()
{
    kitAspectFactoriesStorage().onKitsLoaded();
}

const QList<KitAspectFactory *> KitAspectFactory::kitAspectFactories()
{
    return kitAspectFactoriesStorage().kitAspectFactories();
}

std::optional<QtTaskTree::ExecutableItem> KitAspectFactory::autoDetect(
    Kit *kit,
    const FilePaths &searchPaths,
    const DetectionSource &detectionSource,
    const LogCallback &logCallback) const
{
    Q_UNUSED(kit);
    Q_UNUSED(searchPaths);
    Q_UNUSED(detectionSource);
    Q_UNUSED(logCallback);

    return std::nullopt;
}

std::optional<QtTaskTree::ExecutableItem> KitAspectFactory::removeAutoDetected(
    const QString &detectionSourceId, const LogCallback &logCallback) const
{
    Q_UNUSED(detectionSourceId);
    Q_UNUSED(logCallback);
    return std::nullopt;
}

void KitAspectFactory::listAutoDetected(
    const QString &detectionSourceId, const LogCallback &logCallback) const
{
    Q_UNUSED(detectionSourceId)
    Q_UNUSED(logCallback)
}

Result<QtTaskTree::ExecutableItem> KitAspectFactory::createAspectFromJson(
    const DetectionSource &detectionSource,
    const FilePath &rootPath,
    Kit *kit,
    const QJsonValue &json,
    const LogCallback &logCallback) const
{
    Q_UNUSED(detectionSource);
    Q_UNUSED(kit);
    Q_UNUSED(json);
    Q_UNUSED(logCallback);
    Q_UNUSED(rootPath);
    return ResultError(
        Tr::tr("Kit aspect factory \"%1\" does not support creating aspects from JSON.")
            .arg(id().toString()));
}

using Group = QtTaskTree::Group; // trick lupdate, QTBUG-140636
Group kitDetectionRecipe(
    const IDeviceConstPtr &device,
    DetectionSource::DetectionType detectionType,
    const LogCallback &logCallback)
{
    using namespace QtTaskTree;

    Storage<GroupItems> detectorItems;
    Storage<Kit *> kit;

    const DetectionSource detectionSource{detectionType, device->id().toString()};

    const auto setup = [kit, detectorItems, device, detectionSource, logCallback] {
        const auto root = device->rootPath();

        const FilePaths searchPaths
            = Utils::transform(device->systemEnvironment().path(), [&root](const FilePath &path) {
                  return root.withNewPath(path.path());
              });

        const QString detectionSourceId = device->id().toString();

        logCallback(Tr::tr("Auto detecting kits for device: %1").arg(device->displayName()));

        *kit = KitManager::registerKit([detectionSourceId, device, detectionSource](Kit *k) {
            k->setDetectionSource(detectionSource);
            k->setUnexpandedDisplayName("%{Device:Name}");

            RunDeviceTypeKitAspect::setDeviceTypeId(k, device->type());
            RunDeviceKitAspect::setDevice(k, device);
            BuildDeviceTypeKitAspect::setDeviceTypeId(k, device->type());
            BuildDeviceKitAspect::setDevice(k, device);

            k->setSticky(BuildDeviceKitAspect::id(), true);
            k->setSticky(BuildDeviceTypeKitAspect::id(), true);
        });

        for (const auto &factory : KitAspectFactory::kitAspectFactories()) {
            const auto detector
                = factory->autoDetect(*kit, searchPaths, detectionSource, logCallback);
            if (detector)
                detectorItems->append({*detector});
        }
    };

    const auto setupDetectorTree = [detectorItems](QTaskTree &tree) {
        tree.setRecipe(Group(*detectorItems));
    };

    // clang-format off
    return Group {
        kit, detectorItems,
        QSyncTask(setup),
        QTaskTreeTask(setupDetectorTree),
        QSyncTask([kit,logCallback] {
            if (!(*kit)->isValid()) {
                KitManager::deregisterKit(*kit);
                return;
            }

            // The kit registered in "setup" needs to update the kit aspects
            // found by "setupDetectorTree"
            (*kit)->fix();

            logCallback(Tr::tr("Found kit: %1.").arg((*kit)->displayName()));
        }),
    };
    // clang-format on
}

Group removeDetectedKitsRecipe(const IDeviceConstPtr &device, const LogCallback &logCallback)
{
    using namespace QtTaskTree;

    const auto root = device->rootPath();
    const QString detectionSource = device->id().toString();

    GroupItems removerItems{};
    for (const auto &factory : KitAspectFactory::kitAspectFactories()) {
        const auto remover = factory->removeAutoDetected(detectionSource, logCallback);
        if (remover)
            removerItems.append({*remover});
    }

    const auto removeKits = [device, detectionSource, logCallback]() {
        logCallback(Tr::tr("Removing kits for device: %1.").arg(device->displayName()));

        const auto detectedKits = filtered(KitManager::kits(), [detectionSource](const Kit *k) {
            return k->detectionSource().id == detectionSource;
        });

        for (Kit *kit : detectedKits) {
            logCallback(Tr::tr("Removing kit: %1.").arg(kit->displayName()));
            KitManager::deregisterKit(kit);
        }
    };

    // clang-format off
    return Group {
        QSyncTask(removeKits),
        Group {
            parallelIdealThreadCountLimit,
            removerItems,
        }
    };
    // clang-format on
}

void listAutoDetected(const IDeviceConstPtr &device, const LogCallback &logCallback)
{
    const QString detectionSource = device->id().toString();

    for (const auto kit : KitManager::kits()) {
        if (kit->detectionSource().id == detectionSource)
            logCallback(Tr::tr("Kit: %1.").arg(kit->displayName()));
    }

    for (const auto &factory : KitAspectFactory::kitAspectFactories())
        factory->listAutoDetected(detectionSource, logCallback);
}

QDebug operator<<(QDebug dbg, const DetectionSource &source)
{
    dbg.nospace() << "DetectionSource(";
    dbg.nospace() << "type: " << source.type;
    if (!source.id.isEmpty())
        dbg.nospace() << ", id: " << source.id;
    dbg.nospace() << ")";
    return dbg;
}

} // namespace ProjectExplorer
