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

#include "dockerdevice.h"

#include "dockerapi.h"
#include "dockerconstants.h"
#include "dockerdevicewidget.h"
#include "dockertr.h"
#include "kitdetector.h"

#include <extensionsystem/pluginmanager.h>

#include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h>

#include <projectexplorer/buildconfiguration.h>
#include <projectexplorer/devicesupport/devicemanager.h>
#include <projectexplorer/devicesupport/idevicewidget.h>
#include <projectexplorer/kitinformation.h>
#include <projectexplorer/kitmanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/runcontrol.h>
#include <projectexplorer/target.h>
#include <projectexplorer/toolchain.h>
#include <projectexplorer/toolchainmanager.h>

#include <qtsupport/baseqtversion.h>
#include <qtsupport/qtkitinformation.h>
#include <qtsupport/qtversionfactory.h>
#include <qtsupport/qtversionmanager.h>

#include <utils/algorithm.h>
#include <utils/basetreeview.h>
#include <utils/devicefileaccess.h>
#include <utils/deviceshell.h>
#include <utils/environment.h>
#include <utils/fileutils.h>
#include <utils/hostosinfo.h>
#include <utils/infolabel.h>
#include <utils/layoutbuilder.h>
#include <utils/overridecursor.h>
#include <utils/pathlisteditor.h>
#include <utils/port.h>
#include <utils/processinterface.h>
#include <utils/qtcassert.h>
#include <utils/qtcprocess.h>
#include <utils/sortfiltermodel.h>
#include <utils/temporaryfile.h>
#include <utils/treemodel.h>
#include <utils/utilsicons.h>

#include <QApplication>
#include <QCheckBox>
#include <QComboBox>
#include <QDateTime>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFileSystemWatcher>
#include <QHeaderView>
#include <QHostAddress>
#include <QLoggingCategory>
#include <QNetworkInterface>
#include <QPushButton>
#include <QRandomGenerator>
#include <QRegularExpression>
#include <QTextBrowser>
#include <QThread>
#include <QToolButton>

#include <numeric>

#ifdef Q_OS_UNIX
#include <sys/types.h>
#include <unistd.h>
#endif

using namespace Core;
using namespace ProjectExplorer;
using namespace Utils;

Q_LOGGING_CATEGORY(dockerDeviceLog, "qtc.docker.device", QtWarningMsg);

namespace Docker::Internal {

const QString s_pidMarker = "__qtc$$qtc__";

class ContainerShell : public Utils::DeviceShell
{
public:
    ContainerShell(DockerSettings *settings, const QString &containerId, const FilePath &devicePath)
        : m_settings(settings)
        , m_containerId(containerId)
        , m_devicePath(devicePath)
    {}

private:
    void setupShellProcess(QtcProcess *shellProcess) final
    {
        shellProcess->setCommand({m_settings->dockerBinaryPath.filePath(),
                                  {"container", "start", "-i", "-a", m_containerId}});
    }

    CommandLine createFallbackCommand(const CommandLine &cmdLine)
    {
        CommandLine result = cmdLine;
        result.setExecutable(cmdLine.executable().onDevice(m_devicePath));
        return result;
    }

private:
    DockerSettings *m_settings;
    QString m_containerId;
    FilePath m_devicePath;
};

class DockerDeviceFileAccess : public UnixDeviceFileAccess
{
public:
    DockerDeviceFileAccess(DockerDevicePrivate *dev)
        : m_dev(dev)
    {}

    RunResult runInShell(const CommandLine &cmdLine,
                         const QByteArray &stdInData) const override;
    QString mapToDevicePath(const FilePath &filePath) const override;
    OsType osType(const FilePath &filePath) const override;

    DockerDevicePrivate *m_dev = nullptr;
};

class DockerDevicePrivate : public QObject
{
public:
    DockerDevicePrivate(DockerDevice *parent, DockerSettings *settings, DockerDeviceData data)
        : q(parent)
        , m_data(std::move(data))
        , m_settings(settings)
    {}

    ~DockerDevicePrivate() { stopCurrentContainer(); }

    RunResult runInShell(const CommandLine &cmd, const QByteArray &stdInData = {});

    void updateContainerAccess();
    void changeMounts(QStringList newMounts);
    bool ensureReachable(const FilePath &other);
    void shutdown();

    QString containerId() { return m_container; }
    DockerDeviceData data() { return m_data; }
    void setData(const DockerDeviceData &data);
    DockerSettings *settings() { return m_settings; }

    QString repoAndTag() const { return m_data.repoAndTag(); }
    QString dockerImageId() const { return m_data.imageId; }

    Environment environment();

    CommandLine withDockerExecCmd(const CommandLine &cmd, bool interactive = false);

    bool prepareForBuild(const Target *target);
    Tasks validateMounts() const;

    bool createContainer();
    void startContainer();
    void stopCurrentContainer();
    void fetchSystemEnviroment();

    bool addTemporaryMount(const FilePath &path, const FilePath &containerPath);

    QStringList createMountArgs() const;

    DockerDevice *const q;
    DockerDeviceData m_data;
    DockerSettings *m_settings;

    struct TemporaryMountInfo
    {
        FilePath path;
        FilePath containerPath;
    };

    QList<TemporaryMountInfo> m_temporaryMounts;

    std::unique_ptr<ContainerShell> m_shell;

    QString m_container;

    Environment m_cachedEnviroment;
    bool m_isShutdown = false;
    DockerDeviceFileAccess m_fileAccess{this};
};

class DockerProcessImpl : public Utils::ProcessInterface
{
public:
    DockerProcessImpl(IDevice::ConstPtr device, DockerDevicePrivate *devicePrivate);
    virtual ~DockerProcessImpl();

private:
    void start() override;
    qint64 write(const QByteArray &data) override;
    void sendControlSignal(ControlSignal controlSignal) override;

private:
    CommandLine fullLocalCommandLine(bool interactive);

private:
    DockerDevicePrivate *m_devicePrivate = nullptr;
    // Store the IDevice::ConstPtr in order to extend the lifetime of device for as long
    // as this object is alive.
    IDevice::ConstPtr m_device;

    QtcProcess m_process;
    qint64 m_remotePID = 0;
    bool m_hasReceivedFirstOutput = false;
};

CommandLine DockerProcessImpl::fullLocalCommandLine(bool interactive)
{
    QStringList args;

    if (!m_setup.m_workingDirectory.isEmpty()) {
        FilePath workingDir = m_setup.m_workingDirectory;

        if (DeviceManager::deviceForPath(workingDir) != m_device) {
            QTC_CHECK(m_device->ensureReachable(workingDir));
            workingDir = workingDir.onDevice(m_device->rootPath());
        }

        args.append({"cd", workingDir.path()});
        args.append("&&");
    }

    args.append({"echo", s_pidMarker, "&&"});

    const Environment &env = m_setup.m_environment;
    for (auto it = env.constBegin(); it != env.constEnd(); ++it)
        args.append(env.key(it) + "='" + env.expandedValueForKey(env.key(it)) + '\'');

    args.append("exec");
    args.append({m_setup.m_commandLine.executable().path(), m_setup.m_commandLine.arguments()});

    CommandLine shCmd("/bin/sh", {"-c", args.join(" ")});
    return m_devicePrivate->withDockerExecCmd(shCmd, interactive);
}

DockerProcessImpl::DockerProcessImpl(IDevice::ConstPtr device, DockerDevicePrivate *devicePrivate)
    : m_devicePrivate(devicePrivate)
    , m_device(std::move(device))
    , m_process(this)
{
    connect(&m_process, &QtcProcess::started, this, [this] {
        qCDebug(dockerDeviceLog) << "Process started:" << m_process.commandLine();
    });

    connect(&m_process, &QtcProcess::readyReadStandardOutput, this, [this] {
        if (!m_hasReceivedFirstOutput) {
            QByteArray output = m_process.readAllStandardOutput();
            qsizetype idx = output.indexOf('\n');
            QByteArray firstLine = output.left(idx);
            QByteArray rest = output.mid(idx + 1);
            qCDebug(dockerDeviceLog)
                << "Process first line received:" << m_process.commandLine() << firstLine;
            if (firstLine.startsWith("__qtc")) {
                bool ok = false;
                m_remotePID = firstLine.mid(5, firstLine.size() - 5 - 5).toLongLong(&ok);

                if (ok)
                    emit started(m_remotePID);

                if (rest.size() > 0)
                    emit readyRead(rest, {});

                m_hasReceivedFirstOutput = true;
                return;
            }
        }
        emit readyRead(m_process.readAllStandardOutput(), {});
    });

    connect(&m_process, &QtcProcess::readyReadStandardError, this, [this] {
        emit readyRead({}, m_process.readAllStandardError());
    });

    connect(&m_process, &QtcProcess::done, this, [this] {
        qCDebug(dockerDeviceLog) << "Process exited:" << m_process.commandLine()
                                 << "with code:" << m_process.resultData().m_exitCode;
        emit done(m_process.resultData());
    });
}

DockerProcessImpl::~DockerProcessImpl()
{
    if (m_process.state() == QProcess::Running)
        sendControlSignal(ControlSignal::Kill);
}

void DockerProcessImpl::start()
{
    m_process.setProcessImpl(m_setup.m_processImpl);
    m_process.setProcessMode(m_setup.m_processMode);
    m_process.setTerminalMode(m_setup.m_terminalMode);
    m_process.setReaperTimeout(m_setup.m_reaperTimeout);
    m_process.setWriteData(m_setup.m_writeData);
    m_process.setProcessChannelMode(m_setup.m_processChannelMode);
    m_process.setExtraData(m_setup.m_extraData);
    m_process.setStandardInputFile(m_setup.m_standardInputFile);
    m_process.setAbortOnMetaChars(m_setup.m_abortOnMetaChars);
    if (m_setup.m_lowPriority)
        m_process.setLowPriority();

    m_process.setCommand(fullLocalCommandLine(m_setup.m_processMode == ProcessMode::Writer));
    m_process.start();
}

qint64 DockerProcessImpl::write(const QByteArray &data)
{
    return m_process.writeRaw(data);
}

void DockerProcessImpl::sendControlSignal(ControlSignal controlSignal)
{
    QTC_ASSERT(m_remotePID, return);
    int signal = controlSignalToInt(controlSignal);
    m_devicePrivate->runInShell(
        {"kill", {QString("-%1").arg(signal), QString("%2").arg(m_remotePID)}});
}

IDeviceWidget *DockerDevice::createWidget()
{
    return new DockerDeviceWidget(sharedFromThis());
}

Tasks DockerDevice::validate() const
{
    return d->validateMounts();
}

Tasks DockerDevicePrivate::validateMounts() const
{
    Tasks result;

    for (const QString &mount : m_data.mounts) {
        const FilePath path = FilePath::fromUserInput(mount);
        if (!path.isDir()) {
            const QString message = Tr::tr("Path \"%1\" is not a directory or does not exist.")
                                        .arg(mount);

            result.append(Task(Task::Error, message, {}, -1, {}));
        }
    }
    return result;
}

RunResult DockerDeviceFileAccess::runInShell(const CommandLine &cmdLine,
                                             const QByteArray &stdInData) const
{
    QTC_ASSERT(m_dev, return {});
    return m_dev->runInShell(cmdLine, stdInData);
}

QString DockerDeviceFileAccess::mapToDevicePath(const FilePath &filePath) const
{
    // make sure to convert windows style paths to unix style paths with the file system case:
    // C:/dev/src -> /c/dev/src
    const FilePath normalized = FilePath::fromString(filePath.path()).normalizedPathName();
    QString path = normalized.path();
    if (HostOsInfo::isWindowsHost() && normalized.startsWithDriveLetter()) {
        const QChar lowerDriveLetter = path.at(0);
        path = '/' + lowerDriveLetter + path.mid(2); // strip C:
    }
    return path;
}

OsType DockerDeviceFileAccess::osType(const FilePath &filePath) const
{
    QTC_ASSERT(m_dev, return UnixDeviceFileAccess::osType(filePath));
    return m_dev->q->osType();
}

DockerDevice::DockerDevice(DockerSettings *settings, const DockerDeviceData &data)
    : d(new DockerDevicePrivate(this, settings, data))
{
    setFileAccess(&d->m_fileAccess);
    setDisplayType(Tr::tr("Docker"));
    setOsType(OsTypeOtherUnix);
    setDefaultDisplayName(Tr::tr("Docker Image"));

    setDisplayName(Tr::tr("Docker Image \"%1\" (%2)").arg(data.repoAndTag()).arg(data.imageId));
    setAllowEmptyCommand(true);

    setOpenTerminal([this, settings](const Environment &env, const FilePath &workingDir) {
        Q_UNUSED(env); // TODO: That's the runnable's environment in general. Use it via -e below.
        updateContainerAccess();
        if (d->containerId().isEmpty()) {
            MessageManager::writeDisrupting(Tr::tr("Error starting remote shell. No container."));
            return;
        }

        QtcProcess *proc = new QtcProcess(d);
        proc->setTerminalMode(TerminalMode::On);

        QObject::connect(proc, &QtcProcess::done, [proc] {
            if (proc->error() != QProcess::UnknownError && MessageManager::instance()) {
                MessageManager::writeDisrupting(
                    Tr::tr("Error starting remote shell: %1").arg(proc->errorString()));
            }
            proc->deleteLater();
        });

        const QString wd = workingDir.isEmpty() ? "/" : workingDir.path();
        proc->setCommand({settings->dockerBinaryPath.filePath(),
                          {"exec", "-it", "-w", wd, d->containerId(), "/bin/sh"}});
        proc->setEnvironment(Environment::systemEnvironment()); // The host system env. Intentional.
        proc->start();
    });

    addDeviceAction({Tr::tr("Open Shell in Container"), [](const IDevice::Ptr &device, QWidget *) {
                         device->openTerminal(device->systemEnvironment(), FilePath());
                     }});
}

DockerDevice::~DockerDevice()
{
    delete d;
}

void DockerDevice::shutdown()
{
    d->shutdown();
}

const DockerDeviceData DockerDevice::data() const
{
    return d->data();
}

DockerDeviceData DockerDevice::data()
{
    return d->data();
}

void DockerDevice::setData(const DockerDeviceData &data)
{
    d->setData(data);
}

void DockerDevice::updateContainerAccess() const
{
    d->updateContainerAccess();
}

CommandLine DockerDevicePrivate::withDockerExecCmd(const CommandLine &cmd, bool interactive)
{
    if (!m_settings)
        return {};

    updateContainerAccess();

    QStringList args;

    args << "exec";
    if (interactive)
        args << "-i";
    args << m_container;

    CommandLine dcmd{m_settings->dockerBinaryPath.filePath(), args};
    dcmd.addCommandLineAsArgs(cmd, CommandLine::Raw);
    return dcmd;
}

void DockerDevicePrivate::stopCurrentContainer()
{
    if (!m_settings)
        return;
    if (m_container.isEmpty())
        return;
    if (!DockerApi::isDockerDaemonAvailable(false).value_or(false))
        return;

    m_shell.reset();

    QtcProcess proc;
    proc.setCommand({m_settings->dockerBinaryPath.filePath(), {"container", "stop", m_container}});

    m_container.clear();

    proc.runBlocking();

    m_cachedEnviroment.clear();
}

bool DockerDevicePrivate::prepareForBuild(const Target *target)
{
    QTC_ASSERT(QThread::currentThread() == thread(), return false);

    return ensureReachable(target->project()->projectDirectory())
           && ensureReachable(target->activeBuildConfiguration()->buildDirectory());
}

QString escapeMountPathUnix(const FilePath &fp)
{
    return fp.nativePath().replace('\"', "\"\"");
}

QString escapeMountPathWin(const FilePath &fp)
{
    QString result = fp.nativePath().replace('\"', "\"\"").replace('\\', '/');
    if (result.size() >= 2 && result[1] == ':')
        result = "/" + result[0] + "/" + result.mid(3);
    return result;
}

QString escapeMountPath(const FilePath &fp)
{
    if (HostOsInfo::isWindowsHost())
        return escapeMountPathWin(fp);

    return escapeMountPathUnix(fp);
}

QStringList toMountArg(const DockerDevicePrivate::TemporaryMountInfo &mi)
{
    QString escapedPath;
    QString escapedContainerPath;

    escapedPath = escapeMountPath(mi.path);
    escapedContainerPath = escapeMountPath(mi.containerPath);

    const QString mountArg = QString(R"(type=bind,"source=%1","destination=%2")")
                                 .arg(escapedPath)
                                 .arg(escapedContainerPath);

    return QStringList{"--mount", mountArg};
}

bool isValidMountInfo(const DockerDevicePrivate::TemporaryMountInfo &mi)
{
    if (mi.path.needsDevice())
        return false;

    if (mi.path.isEmpty() || mi.containerPath.isEmpty())
        return false;
    if (!mi.path.isAbsolutePath() || !mi.containerPath.isAbsolutePath())
        return false;

    if (mi.containerPath.isRootPath())
        return false;

    if (!mi.path.exists())
        return false;

    return true;
}

QStringList DockerDevicePrivate::createMountArgs() const
{
    QStringList cmds;
    QList<TemporaryMountInfo> mounts = m_temporaryMounts;
    for (const QString &m : m_data.mounts)
        mounts.append({FilePath::fromUserInput(m), FilePath::fromUserInput(m)});

    for (const TemporaryMountInfo &mi : mounts) {
        if (isValidMountInfo(mi))
            cmds += toMountArg(mi);
    }

    return cmds;
}

bool DockerDevicePrivate::createContainer()
{
    if (!m_settings)
        return false;

    const QString display = HostOsInfo::isLinuxHost() ? QString(":0")
                                                      : QString("host.docker.internal:0");
    CommandLine dockerCreate{m_settings->dockerBinaryPath.filePath(),
                             {"create",
                              "-i",
                              "--rm",
                              "-e",
                              QString("DISPLAY=%1").arg(display),
                              "-e",
                              "XAUTHORITY=/.Xauthority",
                              "--net",
                              "host"}};

#ifdef Q_OS_UNIX
    // no getuid() and getgid() on Windows.
    if (m_data.useLocalUidGid)
        dockerCreate.addArgs({"-u", QString("%1:%2").arg(getuid()).arg(getgid())});
#endif
    FilePath dumperPath = FilePath::fromString("/tmp/qtcreator/debugger");
    addTemporaryMount(Core::ICore::resourcePath("debugger/"), dumperPath);
    q->setDebugDumperPath(dumperPath);

    dockerCreate.addArgs(createMountArgs());

    if (!m_data.keepEntryPoint)
        dockerCreate.addArgs({"--entrypoint", "/bin/sh"});

    dockerCreate.addArg(m_data.repoAndTag());

    qCDebug(dockerDeviceLog).noquote() << "RUNNING: " << dockerCreate.toUserOutput();
    QtcProcess createProcess;
    createProcess.setCommand(dockerCreate);
    createProcess.runBlocking();

    if (createProcess.result() != ProcessResult::FinishedWithSuccess) {
        qCWarning(dockerDeviceLog) << "Failed creating docker container:";
        qCWarning(dockerDeviceLog) << "Exit Code:" << createProcess.exitCode();
        qCWarning(dockerDeviceLog) << createProcess.allOutput();
        return false;
    }

    m_container = createProcess.cleanedStdOut().trimmed();
    if (m_container.isEmpty())
        return false;

    qCDebug(dockerDeviceLog) << "ContainerId:" << m_container;
    return true;
}

void DockerDevicePrivate::startContainer()
{
    if (!createContainer())
        return;

    m_shell = std::make_unique<ContainerShell>(m_settings,
                                               m_container,
                                               FilePath::fromString(
                                                   QString("device://%1/")
                                                       .arg(this->q->id().toString())));

    connect(m_shell.get(), &DeviceShell::done, this, [this](const ProcessResultData &resultData) {
        if (resultData.m_error != QProcess::UnknownError
            || resultData.m_exitStatus == QProcess::NormalExit)
            return;

        qCWarning(dockerDeviceLog) << "Container shell encountered error:" << resultData.m_error;
        m_shell.release()->deleteLater();

        DockerApi::recheckDockerDaemon();
        MessageManager::writeFlashing(Tr::tr("Docker daemon appears to be not running. "
                                             "Verify daemon is up and running and reset the "
                                             "docker daemon on the docker device settings page "
                                             "or restart Qt Creator."));
    });

    if (!m_shell->start()) {
        qCWarning(dockerDeviceLog) << "Container shell failed to start";
    }
}

void DockerDevicePrivate::updateContainerAccess()
{
    if (m_isShutdown)
        return;

    if (DockerApi::isDockerDaemonAvailable(false).value_or(false) == false)
        return;

    if (m_shell)
        return;

    startContainer();
}

void DockerDevice::setMounts(const QStringList &mounts) const
{
    d->changeMounts(mounts);
}

const char DockerDeviceDataImageIdKey[] = "DockerDeviceDataImageId";
const char DockerDeviceDataRepoKey[] = "DockerDeviceDataRepo";
const char DockerDeviceDataTagKey[] = "DockerDeviceDataTag";
const char DockerDeviceDataSizeKey[] = "DockerDeviceDataSize";
const char DockerDeviceUseOutsideUser[] = "DockerDeviceUseUidGid";
const char DockerDeviceMappedPaths[] = "DockerDeviceMappedPaths";
const char DockerDeviceKeepEntryPoint[] = "DockerDeviceKeepEntryPoint";

void DockerDevice::fromMap(const QVariantMap &map)
{
    ProjectExplorer::IDevice::fromMap(map);
    DockerDeviceData data;

    data.repo = map.value(DockerDeviceDataRepoKey).toString();
    data.tag = map.value(DockerDeviceDataTagKey).toString();
    data.imageId = map.value(DockerDeviceDataImageIdKey).toString();
    data.size = map.value(DockerDeviceDataSizeKey).toString();
    data.useLocalUidGid = map.value(DockerDeviceUseOutsideUser, HostOsInfo::isLinuxHost()).toBool();
    data.mounts = map.value(DockerDeviceMappedPaths).toStringList();
    data.keepEntryPoint = map.value(DockerDeviceKeepEntryPoint).toBool();
    d->setData(data);
}

QVariantMap DockerDevice::toMap() const
{
    QVariantMap map = ProjectExplorer::IDevice::toMap();
    DockerDeviceData data = d->data();

    map.insert(DockerDeviceDataRepoKey, data.repo);
    map.insert(DockerDeviceDataTagKey, data.tag);
    map.insert(DockerDeviceDataImageIdKey, data.imageId);
    map.insert(DockerDeviceDataSizeKey, data.size);
    map.insert(DockerDeviceUseOutsideUser, data.useLocalUidGid);
    map.insert(DockerDeviceMappedPaths, data.mounts);
    map.insert(DockerDeviceKeepEntryPoint, data.keepEntryPoint);
    return map;
}

ProcessInterface *DockerDevice::createProcessInterface() const
{
    return new DockerProcessImpl(this->sharedFromThis(), d);
}

bool DockerDevice::canAutoDetectPorts() const
{
    return true;
}

PortsGatheringMethod DockerDevice::portsGatheringMethod() const
{
    return {[this](QAbstractSocket::NetworkLayerProtocol protocol) -> CommandLine {
                // We might encounter the situation that protocol is given IPv6
                // but the consumer of the free port information decides to open
                // an IPv4(only) port. As a result the next IPv6 scan will
                // report the port again as open (in IPv6 namespace), while the
                // same port in IPv4 namespace might still be blocked, and
                // re-use of this port fails.
                // GDBserver behaves exactly like this.

                Q_UNUSED(protocol)

                // /proc/net/tcp* covers /proc/net/tcp and /proc/net/tcp6
                return {filePath("sed"),
                        "-e 's/.*: [[:xdigit:]]*:\\([[:xdigit:]]\\{4\\}\\).*/\\1/g' /proc/net/tcp*",
                        CommandLine::Raw};
            },

            &Port::parseFromSedOutput};
};

DeviceProcessList *DockerDevice::createProcessListModel(QObject *) const
{
    return nullptr;
}

DeviceTester *DockerDevice::createDeviceTester() const
{
    return nullptr;
}

DeviceProcessSignalOperation::Ptr DockerDevice::signalOperation() const
{
    return DeviceProcessSignalOperation::Ptr();
}

DeviceEnvironmentFetcher::Ptr DockerDevice::environmentFetcher() const
{
    return DeviceEnvironmentFetcher::Ptr();
}

bool DockerDevice::usableAsBuildDevice() const
{
    return true;
}

FilePath DockerDevice::mapToGlobalPath(const FilePath &pathOnDevice) const
{
    if (pathOnDevice.needsDevice()) {
        // Already correct form, only sanity check it's ours...
        QTC_CHECK(handlesFile(pathOnDevice));
        return pathOnDevice;
    }

    return FilePath::fromParts(Constants::DOCKER_DEVICE_SCHEME,
                               d->repoAndTag(),
                               pathOnDevice.path());

// The following would work, but gives no hint on repo and tag
//   result.setScheme("docker");
//    result.setHost(d->m_data.imageId);

// The following would work, but gives no hint on repo, tag and imageid
//    result.setScheme("device");
//    result.setHost(id().toString());
}

Utils::FilePath DockerDevice::rootPath() const
{
    return FilePath::fromParts(Constants::DOCKER_DEVICE_SCHEME, d->repoAndTag(), u"/");
}

bool DockerDevice::handlesFile(const FilePath &filePath) const
{
    if (filePath.scheme() == u"device" && filePath.host() == id().toString())
        return true;

    const bool isDockerScheme = filePath.scheme() == Constants::DOCKER_DEVICE_SCHEME;

    if (isDockerScheme && filePath.host() == d->dockerImageId())
        return true;

    if (isDockerScheme && filePath.host() == QString(d->repoAndTag()))
        return true;

    return false;
}

bool DockerDevice::ensureReachable(const FilePath &other) const
{
    if (other.isSameDevice(rootPath()))
        return true;

    if (other.needsDevice())
        return false;

    if (other.isDir())
        return d->ensureReachable(other);
    return d->ensureReachable(other.parentDir());
}

Environment DockerDevice::systemEnvironment() const
{
    return d->environment();
}

void DockerDevice::aboutToBeRemoved() const
{
    KitDetector detector(sharedFromThis());
    detector.undoAutoDetect(id().toString());
}

void DockerDevicePrivate::fetchSystemEnviroment()
{
    updateContainerAccess();

    if (m_shell && m_shell->state() == DeviceShell::State::Succeeded) {
        const RunResult result = runInShell({"env", {}});
        const QString out = QString::fromUtf8(result.stdOut);
        m_cachedEnviroment = Environment(out.split('\n', Qt::SkipEmptyParts), q->osType());
        return;
    }

    QtcProcess proc;
    proc.setCommand(withDockerExecCmd({"env", {}}));
    proc.start();
    proc.waitForFinished();
    const QString remoteOutput = proc.cleanedStdOut();

    m_cachedEnviroment = Environment(remoteOutput.split('\n', Qt::SkipEmptyParts), q->osType());

    const QString remoteError = proc.cleanedStdErr();
    if (!remoteError.isEmpty())
        qCWarning(dockerDeviceLog) << "Cannot read container environment:", qPrintable(remoteError);
}

RunResult DockerDevicePrivate::runInShell(const CommandLine &cmd, const QByteArray &stdInData)
{
    updateContainerAccess();
    QTC_ASSERT(m_shell, return {});
    return m_shell->runInShell(cmd, stdInData);
}

// Factory

class DockerImageItem final : public TreeItem, public DockerDeviceData
{
public:
    DockerImageItem() {}

    QVariant data(int column, int role) const final
    {
        switch (column) {
        case 0:
            if (role == Qt::DisplayRole)
                return repo;
            break;
        case 1:
            if (role == Qt::DisplayRole)
                return tag;
            break;
        case 2:
            if (role == Qt::DisplayRole)
                return imageId;
            break;
        case 3:
            if (role == Qt::DisplayRole)
                return size;
            break;
        }

        return QVariant();
    }
};

class DockerDeviceSetupWizard final : public QDialog
{
public:
    DockerDeviceSetupWizard(DockerSettings *settings)
        : QDialog(ICore::dialogParent())
        , m_settings(settings)
    {
        setWindowTitle(Tr::tr("Docker Image Selection"));
        resize(800, 600);

        m_model.setHeader({"Repository", "Tag", "Image", "Size"});

        m_view = new TreeView;
        QCheckBox *showUnnamedContainers = new QCheckBox(Tr::tr("Show Unnamed Images"));
        QLabel *statusLabel = new QLabel();
        statusLabel->setText(Tr::tr("Loading ..."));
        statusLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
        statusLabel->setAlignment(Qt::AlignCenter);

        m_proxyModel = new SortFilterModel(this);

        m_proxyModel->setFilterRowFunction(
            [showUnnamedContainers, this](int source_row, const QModelIndex &parent) {
                if (showUnnamedContainers->isChecked())
                    return true;

                return m_model.index(source_row, 0, parent).data(Qt::DisplayRole) != "<none>";
            });

        connect(showUnnamedContainers, &QCheckBox::toggled, this, [this]() {
            m_proxyModel->invalidate();
        });

        m_proxyModel->setSourceModel(&m_model);

        m_view->setModel(m_proxyModel);
        m_view->setEnabled(false);
        m_view->header()->setStretchLastSection(true);
        m_view->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
        m_view->setSelectionBehavior(QAbstractItemView::SelectRows);
        m_view->setSelectionMode(QAbstractItemView::SingleSelection);
        m_view->setSortingEnabled(true);
        m_view->sortByColumn(0, Qt::AscendingOrder);
        m_view->setEnabled(false);

        connect(m_view, &QAbstractItemView::doubleClicked, this, &QDialog::accept);

        m_log = new QTextBrowser;
        m_log->setVisible(dockerDeviceLog().isDebugEnabled());

        const QString fail = QString{"Docker: "}
                             + QCoreApplication::translate("Debugger", "Process failed to start.");
        auto errorLabel = new Utils::InfoLabel(fail, Utils::InfoLabel::Error, this);
        errorLabel->setVisible(false);

        m_buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);

        using namespace Layouting;

        Column {
            Stack {
                statusLabel,
                m_view,
            },
            m_log,
            errorLabel,
            Row{showUnnamedContainers, m_buttons},
        }
            .attachTo(this);

        connect(m_buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
        connect(m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
        m_buttons->button(QDialogButtonBox::Ok)->setEnabled(false);

        CommandLine cmd{m_settings->dockerBinaryPath.filePath(),
                        {"images", "--format", "{{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.Size}}"}};
        m_log->append(Tr::tr("Running \"%1\"\n").arg(cmd.toUserOutput()));

        m_process = new QtcProcess(this);
        m_process->setCommand(cmd);

        connect(m_process, &QtcProcess::readyReadStandardOutput, [this] {
            const QString out = QString::fromUtf8(m_process->readAllStandardOutput().trimmed());
            m_log->append(out);
            for (const QString &line : out.split('\n')) {
                const QStringList parts = line.trimmed().split('\t');
                if (parts.size() != 4) {
                    m_log->append(Tr::tr("Unexpected result: %1").arg(line) + '\n');
                    continue;
                }
                auto item = new DockerImageItem;
                item->imageId = parts.at(0);
                item->repo = parts.at(1);
                item->tag = parts.at(2);
                item->size = parts.at(3);
                m_model.rootItem()->appendChild(item);
            }
            m_log->append(Tr::tr("Done."));
        });

        connect(m_process, &Utils::QtcProcess::readyReadStandardError, this, [this] {
            const QString out = Tr::tr("Error: %1").arg(m_process->cleanedStdErr());
            m_log->append(Tr::tr("Error: %1").arg(out));
        });

        connect(m_process, &QtcProcess::done, errorLabel, [errorLabel, this, statusLabel] {
            delete statusLabel;
            if (m_process->result() == ProcessResult::FinishedWithSuccess)
                m_view->setEnabled(true);
            else
                errorLabel->setVisible(true);
        });

        connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, [this] {
            const QModelIndexList selectedRows = m_view->selectionModel()->selectedRows();
            QTC_ASSERT(selectedRows.size() == 1, return);
            m_buttons->button(QDialogButtonBox::Ok)->setEnabled(selectedRows.size() == 1);
        });

        m_process->start();
    }

    IDevice::Ptr device() const
    {
        const QModelIndexList selectedRows = m_view->selectionModel()->selectedRows();
        QTC_ASSERT(selectedRows.size() == 1, return {});
        DockerImageItem *item = m_model.itemForIndex(
            m_proxyModel->mapToSource(selectedRows.front()));
        QTC_ASSERT(item, return {});

        auto device = DockerDevice::create(m_settings, *item);
        device->setupId(IDevice::ManuallyAdded);
        device->setType(Constants::DOCKER_DEVICE_TYPE);
        device->setMachineType(IDevice::Hardware);

        return device;
    }

public:
    TreeModel<DockerImageItem> m_model;
    TreeView *m_view = nullptr;
    SortFilterModel *m_proxyModel = nullptr;
    QTextBrowser *m_log = nullptr;
    QDialogButtonBox *m_buttons;
    DockerSettings *m_settings;

    QtcProcess *m_process = nullptr;
    QString m_selectedId;
};

// Factory

DockerDeviceFactory::DockerDeviceFactory(DockerSettings *settings)
    : IDeviceFactory(Constants::DOCKER_DEVICE_TYPE)
{
    setDisplayName(Tr::tr("Docker Device"));
    setIcon(QIcon());
    setCreator([settings] {
        DockerDeviceSetupWizard wizard(settings);
        if (wizard.exec() != QDialog::Accepted)
            return IDevice::Ptr();
        return wizard.device();
    });
    setConstructionFunction([settings, this] {
        auto device = DockerDevice::create(settings, {});
        QMutexLocker lk(&m_deviceListMutex);
        m_existingDevices.push_back(device);
        return device;
    });
}

void DockerDeviceFactory::shutdownExistingDevices()
{
    QMutexLocker lk(&m_deviceListMutex);
    for (const auto &weakDevice : m_existingDevices) {
        if (QSharedPointer<DockerDevice> device = weakDevice.lock())
            device->shutdown();
    }
}

bool DockerDevicePrivate::addTemporaryMount(const FilePath &path, const FilePath &containerPath)
{
    bool alreadyAdded = anyOf(m_temporaryMounts, [containerPath](const TemporaryMountInfo &info) {
        return info.containerPath == containerPath;
    });
    if (alreadyAdded)
        return false;

    const TemporaryMountInfo newMount{path, containerPath};

    QTC_ASSERT(isValidMountInfo(newMount), return false);

    qCDebug(dockerDeviceLog) << "Adding temporary mount:" << path;
    m_temporaryMounts.append(newMount);
    stopCurrentContainer(); // Force re-start with new mounts.
    return true;
}

Environment DockerDevicePrivate::environment()
{
    if (!m_cachedEnviroment.isValid())
        fetchSystemEnviroment();

    QTC_CHECK(m_cachedEnviroment.isValid());
    return m_cachedEnviroment;
}

void DockerDevicePrivate::shutdown()
{
    m_isShutdown = true;
    m_settings = nullptr;
    stopCurrentContainer();
}

void DockerDevicePrivate::changeMounts(QStringList newMounts)
{
    newMounts.removeDuplicates();
    if (m_data.mounts != newMounts) {
        m_data.mounts = newMounts;
        stopCurrentContainer(); // Force re-start with new mounts.
    }
}

bool DockerDevicePrivate::ensureReachable(const FilePath &other)
{
    for (const QString &mount : m_data.mounts) {
        const FilePath fMount = FilePath::fromString(mount);
        if (other.isChildOf(fMount))
            return true;
    }

    for (const auto &[path, containerPath] : m_temporaryMounts) {
        if (path.path() != containerPath.path())
            continue;

        if (other.isChildOf(path))
            return true;
    }

    addTemporaryMount(other, other);
    return true;
}

void DockerDevicePrivate::setData(const DockerDeviceData &data)
{
    if (m_data != data) {
        m_data = data;

        // Force restart if the container is already running
        if (!m_container.isEmpty()) {
            stopCurrentContainer();
        }
    }
}

bool DockerDevice::prepareForBuild(const Target *target)
{
    return d->prepareForBuild(target);
}

} // namespace Docker::Internal
