// Copyright (C) 2018 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 "rsyncdeploystep.h"

#include "abstractremotelinuxdeploystep.h"
#include "abstractremotelinuxdeployservice.h"
#include "abstractremotelinuxdeploystep.h"
#include "remotelinux_constants.h"
#include "remotelinuxtr.h"

#include <projectexplorer/deploymentdata.h>
#include <projectexplorer/devicesupport/filetransfer.h>
#include <projectexplorer/devicesupport/idevice.h>
#include <projectexplorer/kitinformation.h>
#include <projectexplorer/projectexplorerconstants.h>
#include <projectexplorer/runconfigurationaspects.h>
#include <projectexplorer/target.h>

#include <utils/algorithm.h>
#include <utils/processinterface.h>
#include <utils/qtcprocess.h>

using namespace ProjectExplorer;
using namespace Utils;

namespace RemoteLinux {

class RsyncDeployService : public AbstractRemoteLinuxDeployService
{
public:
    RsyncDeployService()
    {
        connect(&m_mkdir, &QtcProcess::done, this, [this] {
            if (m_mkdir.result() != ProcessResult::FinishedWithSuccess) {
                QString finalMessage = m_mkdir.errorString();
                const QString stdErr = m_mkdir.cleanedStdErr();
                if (!stdErr.isEmpty()) {
                    if (!finalMessage.isEmpty())
                        finalMessage += '\n';
                    finalMessage += stdErr;
                }
                emit errorMessage(Tr::tr("Deploy via rsync: failed to create remote directories:")
                                  + '\n' + finalMessage);
                setFinished();
                return;
            }
            deployFiles();
        });
        connect(&m_mkdir, &QtcProcess::readyReadStandardError, this, [this] {
            emit stdErrData(QString::fromLocal8Bit(m_mkdir.readAllStandardError()));
        });
        connect(&m_fileTransfer, &FileTransfer::progress,
                this, &AbstractRemoteLinuxDeployService::stdOutData);
        connect(&m_fileTransfer, &FileTransfer::done, this, [this](const ProcessResultData &result) {
            auto notifyError = [this](const QString &message) {
                emit errorMessage(message);
                setFinished();
            };
            if (result.m_error == QProcess::FailedToStart)
                notifyError(Tr::tr("rsync failed to start: %1").arg(result.m_errorString));
            else if (result.m_exitStatus == QProcess::CrashExit)
                notifyError(Tr::tr("rsync crashed."));
            else if (result.m_exitCode != 0)
                notifyError(Tr::tr("rsync failed with exit code %1.").arg(result.m_exitCode));
            else
                setFinished();
        });
    }

    void setDeployableFiles(const QList<DeployableFile> &files);
    void setIgnoreMissingFiles(bool ignore) { m_ignoreMissingFiles = ignore; }
    void setFlags(const QString &flags) { m_flags = flags; }

private:
    bool isDeploymentNecessary() const override;

    void doDeploy() override;
    void stopDeployment() override { setFinished(); };

    void createRemoteDirectories();
    void deployFiles();
    void setFinished();

    mutable FilesToTransfer m_files;
    bool m_ignoreMissingFiles = false;
    QString m_flags;
    QtcProcess m_mkdir;
    FileTransfer m_fileTransfer;
};

void RsyncDeployService::setDeployableFiles(const QList<DeployableFile> &files)
{
    m_files.clear();
    for (const DeployableFile &f : files)
        m_files.append({f.localFilePath(), deviceConfiguration()->filePath(f.remoteFilePath())});
}

bool RsyncDeployService::isDeploymentNecessary() const
{
    if (m_ignoreMissingFiles)
        Utils::erase(m_files, [](const FileToTransfer &file) { return !file.m_source.exists(); });
    return !m_files.empty();
}

void RsyncDeployService::doDeploy()
{
    createRemoteDirectories();
}

void RsyncDeployService::createRemoteDirectories()
{
    QStringList remoteDirs;
    for (const FileToTransfer &file : std::as_const(m_files))
        remoteDirs << file.m_target.parentDir().path();
    remoteDirs.sort();
    remoteDirs.removeDuplicates();

    m_mkdir.setCommand({deviceConfiguration()->filePath("mkdir"), QStringList("-p") + remoteDirs});
    m_mkdir.start();
}

void RsyncDeployService::deployFiles()
{
    m_fileTransfer.setTransferMethod(FileTransferMethod::Rsync);
    m_fileTransfer.setRsyncFlags(m_flags);
    m_fileTransfer.setFilesToTransfer(m_files);
    m_fileTransfer.start();
}

void RsyncDeployService::setFinished()
{
    m_mkdir.close();
    m_fileTransfer.stop();
    handleDeploymentDone();
}

// RsyncDeployStep

RsyncDeployStep::RsyncDeployStep(BuildStepList *bsl, Id id)
        : AbstractRemoteLinuxDeployStep(bsl, id)
{
    auto service = new RsyncDeployService;
    setDeployService(service);

    auto flags = addAspect<StringAspect>();
    flags->setDisplayStyle(StringAspect::LineEditDisplay);
    flags->setSettingsKey("RemoteLinux.RsyncDeployStep.Flags");
    flags->setLabelText(Tr::tr("Flags:"));
    flags->setValue(FileTransferSetupData::defaultRsyncFlags());

    auto ignoreMissingFiles = addAspect<BoolAspect>();
    ignoreMissingFiles->setSettingsKey("RemoteLinux.RsyncDeployStep.IgnoreMissingFiles");
    ignoreMissingFiles->setLabel(Tr::tr("Ignore missing files:"),
                                 BoolAspect::LabelPlacement::InExtraLabel);
    ignoreMissingFiles->setValue(false);

    setInternalInitializer([this, service, flags, ignoreMissingFiles] {
        if (BuildDeviceKitAspect::device(kit()) == DeviceKitAspect::device(kit())) {
            // rsync transfer on the same device currently not implemented
            // and typically not wanted.
            return CheckResult::failure(
                Tr::tr("rsync is only supported for transfers between different devices."));
        }
        service->setIgnoreMissingFiles(ignoreMissingFiles->value());
        service->setFlags(flags->value());
        return service->isDeploymentPossible();
    });

    setRunPreparer([this, service] {
        service->setDeployableFiles(target()->deploymentData().allFiles());
    });
}

RsyncDeployStep::~RsyncDeployStep() = default;

Utils::Id RsyncDeployStep::stepId()
{
    return Constants::RsyncDeployStepId;
}

QString RsyncDeployStep::displayName()
{
    return Tr::tr("Deploy files via rsync");
}

} // RemoteLinux
