// 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 "idevice.h"

#include "devicekitaspects.h"
#include "devicemanager.h"
#include "idevice.h"
#include "idevicefactory.h"
#include "sshparameters.h"

#include "../kit.h"
#include "../kitmanager.h"
#include "../projectexplorerconstants.h"
#include "../projectexplorericons.h"
#include "../projectexplorertr.h"
#include "../target.h"

#include <utils/algorithm.h>
#include <utils/async.h>
#include <utils/commandline.h>
#include <utils/devicefileaccess.h>
#include <utils/environment.h>
#include <utils/icon.h>
#include <utils/layoutbuilder.h>
#include <utils/portlist.h>
#include <utils/qtcassert.h>
#include <utils/qtcprocess.h>
#include <utils/synchronizedvalue.h>
#include <utils/url.h>
#include <utils/fsengine/fsengine.h>

#include <QCoreApplication>
#include <QStandardPaths>

#include <QDateTime>
#include <QReadWriteLock>
#include <QStandardItem>
#include <QString>
#include <QVersionNumber>

/*!
 * \class ProjectExplorer::IDevice::DeviceAction
 * \brief The DeviceAction class describes an action that can be run on a device.
 *
 * The description consists of a human-readable string that will be displayed
 * on a button which, when clicked, executes a functor, and the functor itself.
 * This is typically some sort of dialog or wizard, so \a parent widget is provided.
 */

/*!
 * \class ProjectExplorer::IDevice
 * \brief The IDevice class is the base class for all devices.
 *
 * The term \e device refers to some host to which files can be deployed or on
 * which an application can run, for example.
 * In the typical case, this would be some sort of embedded computer connected in some way to
 * the PC on which \QC runs. This class itself does not specify a connection
 * protocol; that
 * kind of detail is to be added by subclasses.
 * Devices are managed by a \c DeviceManager.
 * \sa ProjectExplorer::DeviceManager
 */

/*!
 * \fn Utils::Id ProjectExplorer::IDevice::invalidId()
 * A value that no device can ever have as its internal id.
 */

/*!
 * \fn QString ProjectExplorer::IDevice::displayType() const
 * Prints a representation of the device's type suitable for displaying to a
 * user.
 */

/*!
 * \fn ProjectExplorer::IDeviceWidget *ProjectExplorer::IDevice::createWidget()
 * Creates a widget that displays device information not part of the IDevice base class.
 *        The widget can also be used to let the user change these attributes.
 */

/*!
 * \fn void ProjectExplorer::IDevice::addDeviceAction(const DeviceAction &deviceAction)
 * Adds an actions that can be run on this device.
 * These actions will be available in the \gui Devices options page.
 */

/*!
 * \fn ProjectExplorer::IDevice::Ptr ProjectExplorer::IDevice::clone() const
 * Creates an identical copy of a device object.
 */

using namespace QtTaskTree;
using namespace Utils;

namespace ProjectExplorer {

static Id newId()
{
    return Id::generate();
}

const char TypeKey[] = "OsType";
const char ClientOsTypeKey[] = "ClientOsType";
const char IdKey[] = "InternalId";
const char OriginKey[] = "Origin";
const char MachineTypeKey[] = "Type";
const char VersionKey[] = "Version";
const char ExtraDataKey[] = "ExtraData";

// Connection
const char HostKey[] = "Host";
const char SshPortKey[] = "SshPort";
const char PortsSpecKey[] = "FreePortsSpec";
const char UserNameKey[] = "Uname";
const char AuthKey[] = "Authentication";
const char KeyFileKey[] = "KeyFile";
const char TimeoutKey[] = "Timeout";
const char HostKeyCheckingKey[] = "HostKeyChecking";

const char SshForwardDebugServerPortKey[] = "SshForwardDebugServerPort";
const char LinkDeviceKey[] = "LinkDevice";

using AuthType = SshParameters::AuthenticationType;
const AuthType DefaultAuthType = SshParameters::AuthenticationTypeAll;
const IDevice::MachineType DefaultMachineType = IDevice::Hardware;

const int DefaultTimeout = 10;

namespace Internal {

class IDevicePrivate
{
public:
    IDevicePrivate(IDevice *q)
        : q(q),
          displayName(q),
          sshParametersAspectContainer(q),
          autoDetectInPath(q),
          autoDetectInQtInstallation(q),
          autoDetectQtInstallation(q),
          autoDetectInDirectories(q),
          autoDetectDirectories(q),
          autoCreateKits(q)
    {
        displayName.setSettingsKey("Name");
        displayName.setDisplayStyle(StringAspect::DisplayStyle::LineEditDisplay);

        autoDetectInPath.setSettingsKey("AutoDetectInPath");
        autoDetectInPath.setDefaultValue(true);
        autoDetectInPath.setLabelText(Tr::tr("Search in PATH"));
        autoDetectInPath.setLabelPlacement(BoolAspect::LabelPlacement::Compact);

        autoDetectInQtInstallation.setSettingsKey("AutoDetectInQtInstallation");
        autoDetectInQtInstallation.setDefaultValue(true);
        autoDetectInQtInstallation.setLabelText(Tr::tr("Search in Qt installation"));
        autoDetectInQtInstallation.setLabelPlacement(BoolAspect::LabelPlacement::Compact);

        autoDetectQtInstallation.setSettingsKey("AutoDetectQtInstallation");
        autoDetectQtInstallation.setHistoryCompleter("QtInstallation");
        autoDetectQtInstallation.setPlaceHolderText("Leave empty to search in $HOME/Qt");
        autoDetectQtInstallation.setExpectedKind(PathChooser::ExistingDirectory);
        autoDetectQtInstallation.setEnabler(&autoDetectInQtInstallation);

        autoDetectInDirectories.setSettingsKey("AutoDetectInDirectories");
        autoDetectInDirectories.setDefaultValue(false);
        autoDetectInDirectories.setLabelText(Tr::tr("Search in directories"));
        autoDetectInDirectories.setLabelPlacement(BoolAspect::LabelPlacement::Compact);

        autoDetectDirectories.setSettingsKey("AutoDetectDirectories");
        autoDetectDirectories.setDisplayStyle(StringAspect::LineEditDisplay);
        autoDetectDirectories.setPlaceHolderText(Tr::tr("Semicolon-separated list of directories"));
        autoDetectDirectories.setToolTip(
            Tr::tr("Select the paths on the device that should be scanned for binaries."));
        autoDetectDirectories.setHistoryCompleter("Directories");
        autoDetectDirectories.setEnabler(&autoDetectInDirectories);

        autoCreateKits.setSettingsKey("AutoCreateKits");
        autoCreateKits.setDefaultValue(true);
        autoCreateKits.setLabelText(Tr::tr("Create kits"));
        autoCreateKits.setToolTip(Tr::tr("Set up kits for this device's toolchains."));
        autoCreateKits.setLabelPlacement(BoolAspect::LabelPlacement::Compact);
        autoCreateKits.setVisible(false);

        QObject::connect(&sshParametersAspectContainer, &AspectContainer::applied, q, [this] {
            *sshParameters.writeLocked() = sshParametersAspectContainer.sshParameters();
        });
    }

    FilePaths autoDetectionPaths() const;

    IDevice *q;

    QString displayType;
    Id type;
    IDevice::Origin origin = IDevice::AutoDetected;
    Id id;
    IDevice::MachineType machineType = IDevice::Hardware;
    SynchronizedValue<OsType> osType = OsTypeOther;
    SynchronizedValue<DeviceFileAccessPtr> fileAccess;
    std::function<DeviceFileAccessPtr()> fileAccessFactory;
    int version = 0; // This is used by devices that have been added by the SDK.

    SynchronizedValue<SshParameters> sshParameters;

    QList<Icon> deviceIcons;
    QList<IDevice::DeviceAction> deviceActions;
    Store extraData;
    IDevice::OpenTerminal openTerminal;

    StringAspect displayName;

    SshParametersAspectContainer sshParametersAspectContainer;

    QHash<Id, DeviceToolAspect *> deviceToolAspects;

    quint64 toolDetectionToken = 0;
    int toolDetectionTaskCount = 0;

    bool isTesting = false;

    BoolAspect autoDetectInPath;
    BoolAspect autoDetectInQtInstallation;
    FilePathAspect autoDetectQtInstallation;
    BoolAspect autoDetectInDirectories;
    StringAspect autoDetectDirectories;
    BoolAspect autoCreateKits;
};

} // namespace Internal


// DeviceToolAspect

Id DeviceToolAspect::toolId() const
{
    return m_toolId;
}

void DeviceToolAspect::setToolId(const Id toolId)
{
    m_toolId = toolId;
}

DeviceToolAspect::ToolType DeviceToolAspect::toolType() const
{
    return m_toolType;
}

void DeviceToolAspect::setToolType(ToolType toolType)
{
    m_toolType = toolType;
}

void DeviceToolAspect::addToLayoutImpl(Layouting::Layout &parent)
{
    FilePathAspect::addToLayoutImpl(parent);
    parent.flush();
}

// DeviceToolFactory

static QList<DeviceToolAspectFactory *> theDeviceToolFactories;

DeviceToolAspectFactory::DeviceToolAspectFactory()
{
    theDeviceToolFactories.append(this);
}

DeviceToolAspectFactory::~DeviceToolAspectFactory()
{
    theDeviceToolFactories.removeOne(this);
}

Id DeviceToolAspectFactory::toolId() const
{
    return m_toolId;
}

QStringList DeviceToolAspectFactory::filePattern() const
{
    return m_filePattern;
}

Result<> DeviceToolAspectFactory::check(const DeviceConstRef &device, const FilePath &candidate) const
{
    if (!m_checker)
        return ResultOk;

    return m_checker(device, candidate);
}

/*!
    Returns if the device supports the file transfer \a method.
    The device must set the corresponding SUPPORTS_RSYNC or SUPPORTS_SFTP values, or have a path
    for the rsync tool set.
*/
bool IDevice::supportsFileTransferMethod(FileTransferMethod method) const
{
    switch (method) {
    case FileTransferMethod::Sftp:
        return extraData(Constants::SUPPORTS_SFTP).toBool();
    case FileTransferMethod::Rsync:
        return extraData(Constants::SUPPORTS_RSYNC).toBool();
    case FileTransferMethod::GenericCopy:
        return true;
    }
    QTC_CHECK(false);
    return false;
}

void IDevice::offerKitCreation()
{
    d->autoCreateKits.setVisible(true);
}

bool IDevice::kitCreationEnabled() const
{
    return d->autoCreateKits.isVisible() && d->autoCreateKits.volatileValue();
}

void IDevice::registerToolDetectionTask(quint64 token)
{
    if (token && token == d->toolDetectionToken)
        ++d->toolDetectionTaskCount;
}

void IDevice::deregisterToolDetectionTask(quint64 token)
{
    if (token && token == d->toolDetectionToken) {
        if (--d->toolDetectionTaskCount == 0)
            KitManager::createKitsForBuildDevice(shared_from_this());
    }
}

Result<Environment> IDevice::getUnixEnvironment(const FilePath &scriptToSource) const
{
    if (deviceState() == IDevice::DeviceDisconnected && !isTesting())
        return ResultError(Tr::tr("Device is not connected."));
    return Utils::getUnixEnvironment(filePath("env"), osType(), scriptToSource);
}

void IDevice::requestToolDetection(const FilePaths &searchPaths)
{
    const quint64 token = kitCreationEnabled() ? ++d->toolDetectionToken : 0;
    d->toolDetectionTaskCount = 0;
    registerToolDetectionTask(token);
    emit DeviceManager::instance()->toolDetectionRequested(id(), searchPaths, token);
    deregisterToolDetectionTask(token);
}

FilePaths IDevice::toolSearchPaths() const
{
    return d->autoDetectionPaths();
}

Group IDevice::autoDetectDeviceToolsRecipe()
{
    struct Data
    {
        DeviceToolAspectFactory *factory;
        FilePaths patterns;
        FilePath currentValue;
        FilePaths candidates = {};
    };

    QList<Data> datas;

    for (DeviceToolAspectFactory *factory : std::as_const(theDeviceToolFactories)) {
        FilePaths patterns = FilePaths::fromStrings(factory->filePattern());
        patterns.setSchemeAndHost(rootPath());

        DeviceToolAspect *toolAspect = d->deviceToolAspects.value(factory->toolId());
        QTC_ASSERT(toolAspect, continue);
        datas << Data{factory, patterns, toolAspect->expandedValue()};
    }

    const ListIterator iterator(datas);

    std::weak_ptr<IDevice> weakDevice = shared_from_this();

    const auto onSetupSearch = [weakDevice, iterator](Async<Data> &task) {
        std::shared_ptr<IDevice> device = weakDevice.lock();
        if (!device)
            return;
        const FilePaths detectionPaths = device->toolSearchPaths();
        const FilePath deviceRootPath = device->rootPath();
        const auto searchForTools = [deviceRootPath,
                                     detectionPaths](Data data, IDeviceConstPtr device) -> Data {
            for (const FilePath &pattern : std::as_const(data.patterns)) {
                FilePaths candidates = Utils::filtered(
                    pattern.searchAllInDirectories(detectionPaths), [&](const FilePath &toolPath) {
                        // We assume that check() is thread safe to call.
                        QTC_ASSERT_RESULT(data.factory->check(device, toolPath), return false);
                        return true;
                    });
                candidates = Utils::transform(candidates, [deviceRootPath](const FilePath &path) {
                    if (path.isChildOf(deviceRootPath))
                        return FilePath::fromPathPart(path.path());
                    return path;
                });
                data.candidates.append(candidates);
            }
            if (!data.currentValue.isEmpty()) {
                if (!data.currentValue.isExecutableFile())
                    data.currentValue.clear();
            }
            return data;
        };

        task.setConcurrentCallData(searchForTools, *iterator, device);
    };

    const auto onSearchDone = [weakDevice, iterator](const Async<Data> &task) {
        std::shared_ptr<IDevice> device = weakDevice.lock();
        if (!device)
            return;

        const Data data = task.result();

        DeviceToolAspect *toolAspect = device->d->deviceToolAspects.value(
            iterator->factory->toolId());
        QTC_ASSERT(toolAspect, return);
        toolAspect->setValueAlternatives(data.candidates);

        const FilePath newValue = [&] {
            if (!data.currentValue.isEmpty())
                return data.currentValue;
            if (!data.candidates.isEmpty())
                return data.candidates.front();
            return FilePath{};
        }();

        toolAspect->setValue(newValue);
    };

    // clang-format off
    return For (iterator) >> Do {
        AsyncTask<Data>(onSetupSearch, onSearchDone)
    };
    // clang-format on
}

DeviceToolAspect *DeviceToolAspectFactory::createAspect(const DeviceConstRef &device) const
{
    auto toolAspect = new DeviceToolAspect;
    toolAspect->setToolId(m_toolId);
    toolAspect->setSettingsKey(m_toolId.name());
    toolAspect->setLabelText(m_labelText);
    toolAspect->setToolTip(m_toolTip);
    toolAspect->setPlaceHolderText(Tr::tr("Leave empty to look up executable in $PATH"));
    toolAspect->setHistoryCompleter(m_toolId.name());
    toolAspect->setValidationFunction(
        [device, checker = m_checker](const QString &newValue) -> FancyLineEdit::AsyncValidationFuture {
            return asyncRun([device, checker, newValue]() -> Result<QString> {
                if (!checker)
                    return newValue;
                FilePath path = FilePath::fromUserInput(newValue);
                Result<> result = checker(device, path);
                return result ? newValue : result.error();
            });
        });

    toolAspect->setAllowPathFromDevice(true);
    toolAspect->setExpectedKind(PathChooser::ExistingCommand);
    toolAspect->setBaseDirectory(device.lock()->rootPath());
    toolAspect->setToolType(m_toolType);
    return toolAspect;
}

void DeviceToolAspectFactory::setToolId(const Id &toolId)
{
    m_toolId = toolId;
}

void DeviceToolAspectFactory::setLabelText(const QString &labelText)
{
    m_labelText = labelText;
}

void DeviceToolAspectFactory::setToolTip(const QString &toolTip)
{
    m_toolTip = toolTip;
}

void DeviceToolAspectFactory::setVariablePrefix(const QByteArray &variablePrefix)
{
    m_variablePrefix = variablePrefix;
}

void DeviceToolAspectFactory::setChecker(const Checker &checker)
{
    m_checker = checker;
}

void DeviceToolAspectFactory::setFilePattern(const QStringList &filePattern)
{
    m_filePattern = filePattern;
}

void DeviceToolAspectFactory::setToolType(DeviceToolAspect::ToolType toolType)
{
    m_toolType = toolType;
}

// DeviceTester

DeviceTester::DeviceTester(const IDevice::Ptr &device, QObject *parent)
    : QObject(parent)
    , m_device(device)
{
    m_device->setIsTesting(true);
}

DeviceTester::~DeviceTester()
{
    m_device->setIsTesting(false);
}

IDevice::IDevice()
    : d(new Internal::IDevicePrivate(this))
{
    setAutoApply(false);

    // allowEmptyCommand.setSettingsKey() intentionally omitted, this is not persisted.

    sshForwardDebugServerPort.setSettingsKey(SshForwardDebugServerPortKey);
    sshForwardDebugServerPort.setLabelText(Tr::tr("Use SSH port forwarding for debugging"));
    sshForwardDebugServerPort.setToolTip(
        Tr::tr("Enable debugging on remote targets that cannot expose GDB server ports.\n"
               "The SSH tunneling is used to map the remote GDB server port to localhost.\n"
               "The local and remote ports are determined automatically."));
    sshForwardDebugServerPort.setDefaultValue(false);
    sshForwardDebugServerPort.setLabelPlacement(BoolAspect::LabelPlacement::AtCheckBox);

    linkDevice.setSettingsKey(LinkDeviceKey);
    linkDevice.setLabelText(Tr::tr("Access via:"));
    linkDevice.setToolTip(Tr::tr("Select the device to connect through."));
    linkDevice.setDefaultValue("direct");
    linkDevice.setComboBoxEditable(false);
    linkDevice.setFillCallback([this](const StringSelectionAspect::ResultCallback &cb) {
        QList<QStandardItem *> items;
        auto defaultItem = new QStandardItem(Tr::tr("Direct"));
        defaultItem->setData("direct");
        items.append(defaultItem);
        for (int i = 0, n = DeviceManager::deviceCount(); i < n; ++i) {
            const auto device = DeviceManager::deviceAt(i);
            if (device->id() == this->id())
                continue;
            QStandardItem *newItem = new QStandardItem(device->displayName());
            newItem->setData(device->id().toSetting());
            items.append(newItem);
        }
        cb(items);
    });

    auto validateDisplayName = [](const QString &old, const QString &newValue) -> Result<> {
        if (old == newValue)
            return ResultOk;

        if (newValue.trimmed().isEmpty())
            return ResultError(Tr::tr("The device name cannot be empty."));

        if (DeviceManager::hasDevice(newValue))
            return ResultError(Tr::tr("A device with this name already exists."));

        return ResultOk;
    };

    d->displayName.setValidationFunction(
        [this, validateDisplayName](const QString &text) -> Result<> {
            return validateDisplayName(d->displayName.value(), text);
        });

    d->displayName.setValueAcceptor(
        [validateDisplayName](const QString &old,
                              const QString &newValue) -> std::optional<QString> {
            if (!validateDisplayName(old, newValue))
                return std::nullopt;

            return newValue;
        });

    freePortsAspect.setSettingsKey(PortsSpecKey);
    freePortsAspect.setLabelText(Tr::tr("Free ports:"));
    freePortsAspect.setToolTip(
        Tr::tr("Enter lists and ranges like this: \"1024,1026-1028,1030\"."));
    freePortsAspect.setHistoryCompleter("PortRange");
}

IDevice::~IDevice() = default;

void IDevice::initDeviceToolAspects()
{
    // shared_from_this doesn't work in the ctor.
    for (const DeviceToolAspectFactory *factory : theDeviceToolFactories) {
        DeviceToolAspect *toolAspect = factory->createAspect(shared_from_this());
        registerAspect(toolAspect, true);
        toolAspect->setBaseDirectory([this] { return rootPath(); });
        d->deviceToolAspects.insert(factory->toolId(), toolAspect);
    }
}

void IDevice::setOpenTerminal(const IDevice::OpenTerminal &openTerminal)
{
    d->openTerminal = openTerminal;
}

void IDevice::setupId(Origin origin, Id id)
{
    d->origin = origin;
    QTC_CHECK(origin == ManuallyAdded || id.isValid());
    d->id = id.isValid() ? id : newId();
}

bool IDevice::canOpenTerminal() const
{
    return bool(d->openTerminal);
}

void IDevice::openTerminal(const Environment &env,
                           const FilePath &workingDir,
                           const Continuation<> &cont) const
{
    QTC_ASSERT(canOpenTerminal(),
               cont(ResultError(Tr::tr("Opening a terminal is not supported."))); return);
    tryToConnect(Continuation<>([=, this](const Result<> &res) {
        if (res)
            d->openTerminal(env, workingDir, cont);
        else
            cont(ResultError(res.error()));
    }));
}

bool IDevice::isAnyUnixDevice() const
{
    return d->osType == OsTypeLinux || d->osType == OsTypeMac || d->osType == OsTypeOtherUnix;
}

DeviceFileAccessPtr IDevice::fileAccess() const
{
    if (d->fileAccessFactory)
        return d->fileAccessFactory();

    return *d->fileAccess.readLocked();
}

bool IDevice::supportsFileAccess() const
{
    return d->fileAccessFactory || *d->fileAccess.readLocked();
}

void IDevice::tryToConnect(const Continuation<> &cont) const
{
    cont(ResultOk);
}

FilePath IDevice::filePath(const QString &pathOnDevice) const
{
    return rootPath().withNewPath(pathOnDevice);
}

Result<> IDevice::handlesFile(const FilePath &filePath) const
{
    if (filePath.scheme() == u"device" && filePath.host() == id().toString())
        return ResultOk;
    return ResultError(
        Tr::tr("The file \"%1\" cannot be handled by the device \"%2\".")
            .arg(filePath.toUserOutput())
            .arg(displayName()));
}

FilePath IDevice::searchExecutableInPath(const QString &fileName) const
{
    FilePaths paths;
    for (const FilePath &pathEntry : systemEnvironment().path())
        paths.append(filePath(pathEntry.path()));
    return searchExecutable(fileName, paths);
}

FilePath IDevice::searchExecutable(const QString &fileName, const FilePaths &dirs) const
{
    for (FilePath dir : dirs) {
        if (!handlesFile(dir)) // Allow device-local dirs to be used.
            dir = filePath(dir.path());
        QTC_CHECK(handlesFile(dir));
        const FilePath candidate = dir / fileName;
        if (candidate.isExecutableFile())
            return candidate;
    }

    return {};
}

ProcessInterface *IDevice::createProcessInterface() const
{
    return nullptr;
}

FileTransferInterface *IDevice::createFileTransferInterface(
        const FileTransferSetupData &setup) const
{
    Q_UNUSED(setup)
    QTC_CHECK(false);
    return nullptr;
}

Environment IDevice::systemEnvironment() const
{
    Result<Environment> env = systemEnvironmentWithError();
    QTC_ASSERT_RESULT(env, return {});
    return *env;
}

Result<Environment> IDevice::systemEnvironmentWithError() const
{
    DeviceFileAccessPtr access = fileAccess();
    QTC_ASSERT(access, return Environment::systemEnvironment());
    return access->deviceEnvironment();
}

Utils::Result<Environment> IDevice::sourcedEnvironment(const Utils::FilePath &script) const
{
    Q_UNUSED(script)
    return ResultError(Tr::tr("Sourcing a script is not supported on this platform."));
}

QString IDevice::displayType() const
{
    return d->displayType;
}

void IDevice::setDisplayType(const QString &type)
{
    d->displayType = type;
}

void IDevice::setOsType(OsType osType)
{
    d->osType = osType;
}

void IDevice::setFileAccess(DeviceFileAccessPtr fileAccess, bool announce)
{
    d->fileAccess.writeLocked()->swap(fileAccess);
    Utils::FSEngine::invalidateFileInfoCache();
    if (announce)
        emit DeviceManager::instance()->deviceUpdated(id());
}

void IDevice::setFileAccessFactory(std::function<DeviceFileAccessPtr()> fileAccessFactory)
{
    d->fileAccessFactory = fileAccessFactory;
}

IDevice::DeviceInfo IDevice::deviceInformation() const
{
    const QString key = Tr::tr("Device");
    return DeviceInfo() << IDevice::DeviceInfoItem(key, deviceStateToString());
}

/*!
    Identifies the type of the device. Devices with the same type share certain
    abilities. This attribute is immutable.

    \sa ProjectExplorer::IDeviceFactory
 */

Id IDevice::type() const
{
    return d->type;
}

void IDevice::setType(Id type)
{
    d->type = type;
}

/*!
    Returns \c true if the device has been added via some sort of auto-detection
    mechanism. Devices that are not auto-detected can only ever be created
    interactively from the \gui Options page. This attribute is immutable.

    \sa DeviceSettingsWidget
*/

bool IDevice::isAutoDetected() const
{
    return d->origin == AutoDetected || isFromSdk();
}

/*!
    Returns \c true if the device has been added by the sdktool. This normally implies it was
    set up by the installer aka Qt Maintenance tool.
*/
bool IDevice::isFromSdk() const
{
    return d->origin == AddedBySdk;
}

/*!
    Identifies the device. If an id is given when constructing a device then
    this id is used. Otherwise, a UUID is generated and used to identity the
    device.

    \sa ProjectExplorer::DeviceManager::findInactiveAutoDetectedDevice()
*/

Id IDevice::id() const
{
    return d->id;
}

QList<Task> IDevice::validate() const
{
    return {};
}

void IDevice::addDeviceAction(const DeviceAction &deviceAction)
{
    d->deviceActions.append(deviceAction);
}

const QList<IDevice::DeviceAction> IDevice::deviceActions() const
{
    return d->deviceActions;
}

ExecutableItem IDevice::portsGatheringRecipe(const Storage<PortsOutputData> &output) const
{
    const Storage<PortsInputData> input;

    const auto onSetup = [this, input] {
        const CommandLine cmd = filePath("/proc/net").isReadableDir()
                              ? CommandLine{filePath("/bin/sh"), {"-c", "cat /proc/net/tcp*"}}
                              : CommandLine{filePath("netstat"), {"-a", "-n"}};
        *input = {freePorts(), cmd};
    };

    return Group {
        input,
        onGroupSetup(onSetup),
        portsFromProcessRecipe(input, output)
    };
}

DeviceTester *IDevice::createDeviceTester()
{
    QTC_ASSERT(false, qDebug("This should not have been called..."));
    return nullptr;
}

void IDevice::setIsTesting(bool isTesting)
{
    d->isTesting = isTesting;
}

bool IDevice::isTesting() const
{
    return d->isTesting;
}

bool IDevice::canMount(const FilePath &) const
{
    return false;
}

OsType IDevice::osType() const
{
    return d->osType.get();
}

ExecutableItem IDevice::signalOperationRecipe(
    const SignalOperationData &data,
    const Storage<Utils::Result<>> &resultStorage) const
{
    Q_UNUSED(data)
    return QSyncTask([resultStorage] {
        *resultStorage = ResultError(
            Tr::tr("No signal operation recipe is available for this device."));
        return DoneResult::Error;
    });
}

void IDevice::setDeviceState(DeviceState deviceState)
{
    DeviceManager::setDeviceState(id(), deviceState);
}

IDevice::DeviceState IDevice::deviceState() const
{
    return DeviceManager::deviceState(id());
}

QIcon IDevice::overlayIcon() const
{
    switch (deviceState()) {
    case IDevice::DeviceStateUnknown:
        return QIcon();
    case IDevice::DeviceReadyToUse: {
        static const QIcon ready = Icons::DEVICE_READY_INDICATOR_OVERLAY.icon();
        return ready;
    }
    case IDevice::DeviceConnected: {
        static const QIcon connected = Icons::DEVICE_CONNECTED_INDICATOR_OVERLAY.icon();
        return connected;
    }
    case IDevice::DeviceDisconnected: {
        static const QIcon disconnected = Icons::DEVICE_DISCONNECTED_INDICATOR_OVERLAY.icon();
        return disconnected;
    }
    }
    return QIcon();
}

Id IDevice::typeFromMap(const Store &map)
{
    return Id::fromSetting(map.value(TypeKey));
}

Id IDevice::idFromMap(const Store &map)
{
    return Id::fromSetting(map.value(IdKey));
}

// Backwards compatibility: Pre 17.0 a bunch of settings were stored in the extra data
namespace {

static const char LinkDevice[] = "RemoteLinux.LinkDevice";
static const char SSH_FORWARD_DEBUGSERVER_PORT[] = "RemoteLinux.SshForwardDebugServerPort";

static void backwardsFromExtraData(IDevice *device, const Store &map)
{
    if (map.contains(LinkDevice))
        device->linkDevice.setValue(Id::fromSetting(map.value(LinkDevice)).toString());

    if (map.contains(SSH_FORWARD_DEBUGSERVER_PORT))
        device->sshForwardDebugServerPort.setValue(map.value(SSH_FORWARD_DEBUGSERVER_PORT).toBool());
}

static void backwardsToExtraData(const IDevice *const device, Store &map)
{
    if (device->linkDevice() != "direct")
        map.insert(LinkDevice, QVariant::fromValue(device->linkDevice()));
    map.insert(SSH_FORWARD_DEBUGSERVER_PORT, device->sshForwardDebugServerPort());
}

} // namespace

/*!
    Restores a device object from a serialized state as written by toMap().
    If subclasses override this to restore additional state, they must call the
    base class implementation.
*/
void IDevice::fromMap(const Store &map)
{
    AspectContainer::fromMap(map);
    d->type = typeFromMap(map);

    d->id = Id::fromSetting(map.value(IdKey));
    d->osType = osTypeFromString(map.value(ClientOsTypeKey).toString()).value_or(OsTypeLinux);
    if (!d->id.isValid())
        d->id = newId();
    d->origin = static_cast<Origin>(map.value(OriginKey, ManuallyAdded).toInt());

    d->machineType = static_cast<MachineType>(map.value(MachineTypeKey, DefaultMachineType).toInt());
    d->version = map.value(VersionKey, 0).toInt();

    d->extraData = storeFromVariant(map.value(ExtraDataKey));

    backwardsFromExtraData(this, d->extraData);

    SshParameters ssh;
    ssh.setHost(map.value(HostKey).toString());
    ssh.setPort(map.value(SshPortKey, 22).toInt());
    ssh.setUserName(map.value(UserNameKey).toString());

    // Pre-4.9, the authentication enum used to have more values
    const int storedAuthType = map.value(AuthKey, DefaultAuthType).toInt();
    const bool outdatedAuthType = storedAuthType > SshParameters::AuthenticationTypeSpecificKey;
    ssh.setAuthenticationType(
        outdatedAuthType ? SshParameters::AuthenticationTypeAll
                         : static_cast<AuthType>(storedAuthType));

    ssh.setPrivateKeyFile(
        FilePath::fromSettings(map.value(KeyFileKey, defaultPrivateKeyFilePath())));
    ssh.setTimeout(map.value(TimeoutKey, DefaultTimeout).toInt());
    SshHostKeyCheckingMode hostKeyCheckingMode = static_cast<SshHostKeyCheckingMode>(
            map.value(HostKeyCheckingKey, SshHostKeyCheckingNone).toInt());
    if (hostKeyCheckingMode < SshHostKeyCheckingNone || hostKeyCheckingMode > SshHostKeyCheckingAllowNoMatch)
        hostKeyCheckingMode = SshHostKeyCheckingNone;
    ssh.setHostKeyCheckingMode(hostKeyCheckingMode);


    d->sshParametersAspectContainer.setSshParameters(ssh);
}

/*!
    Serializes a device object, for example to save it to a file.
    If subclasses override this function to save additional state, they must
    call the base class implementation.
*/

void IDevice::toMap(Store &map) const
{
    AspectContainer::toMap(map);

    map.insert(TypeKey, d->type.toString());
    map.insert(ClientOsTypeKey, osTypeToString(d->osType.get()));
    map.insert(IdKey, d->id.toSetting());
    map.insert(OriginKey, d->origin);

    map.insert(MachineTypeKey, d->machineType);
    map.insert(VersionKey, d->version);

    Store extraData = d->extraData;
    backwardsToExtraData(this, extraData);

    map.insert(ExtraDataKey, variantFromStore(extraData));

    SshParameters ssh = d->sshParametersAspectContainer.sshParameters();
    map.insert(HostKey, ssh.host());
    map.insert(SshPortKey, ssh.port());
    map.insert(UserNameKey, ssh.userName());
    map.insert(AuthKey, ssh.authenticationType());
    map.insert(KeyFileKey, ssh.privateKeyFile().toSettings());
    map.insert(TimeoutKey, ssh.timeout());
    map.insert(HostKeyCheckingKey, ssh.hostKeyCheckingMode());
}

QString IDevice::displayName() const
{
    return d->displayName();
}

void IDevice::setDisplayName(const QString &name)
{
    d->displayName.setValue(name);
}

QString IDevice::defaultDisplayName() const
{
    return d->displayName.defaultValue();
}

void IDevice::setDefaultDisplayName(const QString &name)
{
    d->displayName.setDefaultValue(name);
}

void IDevice::addDisplayNameToLayout(Layouting::Layout &layout) const
{
    d->displayName.addToLayout(layout);
}

QString IDevice::deviceStateToString() const
{
    switch (deviceState()) {
    case IDevice::DeviceReadyToUse: return Tr::tr("Ready to use");
    case IDevice::DeviceConnected: return Tr::tr("Connected");
    case IDevice::DeviceDisconnected: return Tr::tr("Disconnected");
    case IDevice::DeviceStateUnknown: return Tr::tr("Unknown");
    default: return Tr::tr("Invalid");
    }
}

QPixmap IDevice::deviceStateIcon() const
{
    switch (deviceState()) {
    case IDevice::DeviceReadyToUse: return Icons::DEVICE_READY_INDICATOR.pixmap();
    case IDevice::DeviceConnected: return Icons::DEVICE_CONNECTED_INDICATOR.pixmap();
    case IDevice::DeviceDisconnected: return Icons::DEVICE_DISCONNECTED_INDICATOR.pixmap();
    case IDevice::DeviceStateUnknown: break;
    }
    return {};
}

SshParameters IDevice::sshParameters() const
{
    return *d->sshParameters.readLocked();
}

void IDevice::setDefaultSshParameters(const SshParameters &sshParameters)
{
    QTC_ASSERT(QThread::isMainThread(), return);

    sshParametersAspectContainer().host.setDefaultValue(sshParameters.host());
    sshParametersAspectContainer().port.setDefaultValue(sshParameters.port());
    sshParametersAspectContainer().userName.setDefaultValue(sshParameters.userName());
    sshParametersAspectContainer().privateKeyFile.setDefaultPathValue(
        sshParameters.privateKeyFile());
    sshParametersAspectContainer().timeout.setDefaultValue(sshParameters.timeout());
    sshParametersAspectContainer().useKeyFile.setDefaultValue(
        sshParameters.authenticationType() == SshParameters::AuthenticationTypeSpecificKey);
    sshParametersAspectContainer().hostKeyCheckingMode.setDefaultValue(
        sshParameters.hostKeyCheckingMode());

    *d->sshParameters.writeLocked() = sshParametersAspectContainer().sshParameters();
}

QUrl IDevice::toolControlChannel(const ControlChannelHint &) const
{
    QUrl url;
    url.setScheme(urlTcpScheme());
    url.setHost(d->sshParameters.readLocked()->host());
    return url;
}

void IDevice::setFreePorts(const PortList &freePorts)
{
    freePortsAspect.setPortList(freePorts);
}

PortList IDevice::freePorts() const
{
    return freePortsAspect.portList();
}

IDevice::MachineType IDevice::machineType() const
{
    return d->machineType;
}

void IDevice::setMachineType(MachineType machineType)
{
    d->machineType = machineType;
}

FilePath IDevice::deviceToolPath(Id toolId) const
{
    DeviceToolAspect *toolAspect = d->deviceToolAspects.value(toolId);
    QTC_ASSERT(toolAspect, return {});
    FilePath filePath = (*toolAspect)();
    if (filePath.isEmpty())
        return {};
    if (filePath.isLocal())
        return rootPath().withNewMappedPath(filePath);
    return filePath;
}

FilePath IDevice::deviceToolPath(Id toolId, const FilePath &deviceHint)
{
    IDevice::ConstPtr dev = DeviceManager::deviceForPath(deviceHint);
    QTC_ASSERT(dev, return {});
    return dev->deviceToolPath(toolId);
}

QList<DeviceToolAspect *> IDevice::deviceToolAspects(DeviceToolAspect::ToolType supportType) const
{
    const QList<DeviceToolAspect *> list =
        filtered(d->deviceToolAspects.values(), [supportType](DeviceToolAspect *aspect) {
            return aspect->toolType() & supportType;
        });

    return Utils::sorted(list, [](DeviceToolAspect *left, DeviceToolAspect *right) {
        return left->labelText().toCaseFolded() < right->labelText().toCaseFolded();
    });
}

std::function<void(Layouting::Layout *)> IDevice::deviceToolsGui()
{
    using namespace Layouting;
    return [this](Layout *layout) {
        layout->addItems({
            Column { Space(20) }, br,
            Layouting::Group {
                title(Tr::tr("Run Tools on This Device")),
                Form {
                    deviceToolAspects(DeviceToolAspect::RunTool)
                }
            }, br,
            Layouting::Group {
                title(Tr::tr("Source and Build Tools on This Device")),
                Form {
                    deviceToolAspects(DeviceToolAspect::ToolType(
                        DeviceToolAspect::SourceTool | DeviceToolAspect::BuildTool))
                }
            }, br,
            Layouting::Group {
                title(Tr::tr("Auto-Detection")),
                Column {
                    Grid {
                        d->autoDetectInPath, br,
                        d->autoDetectInQtInstallation, d->autoDetectQtInstallation, br,
                        d->autoDetectInDirectories, d->autoDetectDirectories, br,
                        d->autoCreateKits, br,
                    },
                }
            }, br,
        });
    };
}

FilePath IDevice::rootPath() const
{
    // match DeviceManager::deviceForPath
    return FilePath::fromParts(u"device", id().toString(), u"/");
}

void IDevice::setExtraData(Id kind, const QVariant &data)
{
    d->extraData.insert(keyFromString(kind.toString()), data);
}

QVariant IDevice::extraData(Id kind) const
{
    return d->extraData.value(keyFromString(kind.toString()));
}

int IDevice::version() const
{
    return d->version;
}

void IDevice::setFromSdk()
{
    d->origin = AddedBySdk;
}

QString IDevice::defaultPrivateKeyFilePath()
{
    return QStandardPaths::writableLocation(QStandardPaths::HomeLocation)
        + QLatin1String("/.ssh/id_rsa");
}

QString IDevice::defaultPublicKeyFilePath()
{
    return defaultPrivateKeyFilePath() + QLatin1String(".pub");
}

Utils::Result<> IDevice::ensureReachable(const FilePath &other) const
{
    return handlesFile(other); // Some first approximation.
}

Result<FilePath> IDevice::localSource(const FilePath &other) const
{
    Q_UNUSED(other)
    return make_unexpected(Tr::tr("localSource() not implemented for this device type."));
}

bool IDevice::prepareForBuild(const Target *target)
{
    Q_UNUSED(target)
    return true;
}

void IDevice::doApply() const
{
    const_cast<IDevice *>(this)->apply();
}

Result<> SignalOperationData::isValid() const
{
    switch (mode) {
    case SignalOperationMode::KillByPath:
        if (filePath.isEmpty())
            return ResultError(Tr::tr("No path specified for SignalOperationData."));
        break;
    case SignalOperationMode::KillByPid:
    case SignalOperationMode::InterruptByPid:
        if (pid <= 0)
            return ResultError(Tr::tr("No valid PID specified for SignalOperationData."));
        break;
    }
    return ResultOk;
}

// DeviceConstRef

DeviceConstRef::DeviceConstRef(const IDevice::ConstPtr &device)
    : m_constDevice(device)
{}

DeviceConstRef::DeviceConstRef(const IDevice::Ptr &device)
    : m_constDevice(device)
{}

IDevice::ConstPtr DeviceConstRef::lock() const
{
    return m_constDevice.lock();
}

DeviceConstRef::~DeviceConstRef() = default;

Id DeviceConstRef::id() const
{
    const IDevice::ConstPtr device = m_constDevice.lock();
    QTC_ASSERT(device, return {});
    return device->id();
}

QString DeviceConstRef::displayName() const
{
    const IDevice::ConstPtr device = m_constDevice.lock();
    QTC_ASSERT(device, return {});
    return device->displayName();
}

SshParameters DeviceConstRef::sshParameters() const
{
    const IDevice::ConstPtr device = m_constDevice.lock();
    QTC_ASSERT(device, return {});
    return device->sshParameters();
}

QVariant DeviceConstRef::extraData(Id kind) const
{
    const IDevice::ConstPtr device = m_constDevice.lock();
    QTC_ASSERT(device, return {});
    return device->extraData(kind);
}

Id DeviceConstRef::linkDeviceId() const
{
    const IDevice::ConstPtr device = m_constDevice.lock();
    QTC_ASSERT(device, return {});
    return Id::fromString(device->linkDevice.value());
}

FilePath DeviceConstRef::filePath(const QString &pathOnDevice) const
{
    const IDevice::ConstPtr device = m_constDevice.lock();
    QTC_ASSERT(device, return {});
    return device->filePath(pathOnDevice);
}

// DeviceRef, mutable

DeviceRef::DeviceRef(const IDevice::Ptr &device)
    : DeviceConstRef(device), m_mutableDevice(device)
{}

IDevice::Ptr DeviceRef::lock() const
{
    return m_mutableDevice.lock();
}

void DeviceRef::setDisplayName(const QString &displayName)
{
    const IDevice::Ptr device = m_mutableDevice.lock();
    QTC_ASSERT(device, return);
    device->setDisplayName(displayName);
}

void DeviceRef::setSshParameters(const SshParameters &params)
{
    const IDevice::Ptr device = m_mutableDevice.lock();
    QTC_ASSERT(device, return);
    device->sshParametersAspectContainer().setSshParameters(params);
}

SshParametersAspectContainer &IDevice::sshParametersAspectContainer() const
{
    return d->sshParametersAspectContainer;
}

bool IDevice::supportsQtTargetDeviceType(const QSet<Id> &targetDeviceTypes) const
{
    return targetDeviceTypes.contains(type());
}

FilePaths Internal::IDevicePrivate::autoDetectionPaths() const
{
    FilePaths paths;
    if (autoDetectInPath.volatileValue())
        paths += q->systemEnvironment().path();

    if (autoDetectInQtInstallation.volatileValue()) {
        QString qtPath = autoDetectQtInstallation.volatileValue();
        if (qtPath.isEmpty())
            qtPath = q->systemEnvironment().value("HOME") + "/Qt";

        using VersionAndPath = QPair<QVersionNumber, FilePath>;
        QList<VersionAndPath> qtBinPaths;

        // We are looking for something like ~/Qt/6.6.3/gcc_64/bin/
        const FilePath qtInstallation = q->filePath(qtPath);
        for (const FilePath &qtVersion : qtInstallation.dirEntries(QDir::Dirs | QDir::NoDotAndDotDot)) {
            if (qtVersion.fileName().count(".") == 2) {
                const QVersionNumber qtVersionNumber = QVersionNumber::fromString(qtVersion.fileName());
                for (const FilePath &qtArch : qtVersion.dirEntries(QDir::Dirs | QDir::NoDotAndDotDot)) {
                    const FilePath qtBinPath = qtArch.pathAppended("bin");
                    if (qtBinPath.exists())
                        qtBinPaths += std::make_pair(qtVersionNumber, qtBinPath);
                }
            }
        }

        // Prefer higher Qt versions.
        sort(qtBinPaths, [](const VersionAndPath &a, const VersionAndPath &b) {
            return a.first > b.first;
        });

        for (const VersionAndPath &vp : qtBinPaths)
            paths += vp.second;
    }

    if (autoDetectInDirectories.volatileValue()) {
        for (const QString &path : autoDetectDirectories.volatileValue().split(';'))
            paths.append(FilePath::fromString(path.trimmed()));
    }

    paths = transform(paths, [this](const FilePath &path) { return q->filePath(path.path()); });

    return paths;
}

Result<> IDevice::supportsBuildingProject(const FilePath &projectDir) const
{
    return handlesFile(projectDir);
}

} // namespace ProjectExplorer
