/****************************************************************************
**
** Copyright (C) 2012 - 2014 Jolla Ltd.
** Contact: http://jolla.com/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid Commercial Qt licenses may use this file in
** accordance with the Commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia.  For licensing terms and
** conditions see http://qt.digia.com/licensing.  For further information
** use the contact form at http://qt.digia.com/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights.  These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
****************************************************************************/

#include "nxtdeploysteps.h"

#include <nxtconstants.h>
#include <steps/deploy/nxtdeployconfiguration.h>
#include <qtc/nxtsdkkitinformation.h>
#include <qtc/nxtsdkmanager.h>
#include <nxtsettings.h>
#include <qtc/nxttargetkitinformation.h>
#include <vm/nxtvirtualboxmanager.h>
#include <connection/rest/restconnection.h>

#include <coreplugin/fileutils.h>
#include <coreplugin/icore.h>
#include <coreplugin/idocument.h>
#include <extensionsystem/pluginmanager.h>
#include <projectexplorer/buildconfiguration.h>
#include <projectexplorer/buildstep.h>
#include <projectexplorer/buildsteplist.h>
#include <projectexplorer/kitinformation.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectexplorerconstants.h>
#include <projectexplorer/projectnodes.h>
#include <projectexplorer/target.h>
#include <qmakeprojectmanager/qmakebuildconfiguration.h>
#include <qtsupport/baseqtversion.h>
#include <qtsupport/qtkitinformation.h>
#include <remotelinux/abstractremotelinuxdeployservice.h>
#include <ssh/sshremoteprocessrunner.h>
#include <utils/macroexpander.h>
#include <utils/qtcassert.h>

#include <QApplication>
#include <QClipboard>
#include <QMimeData>
#include <QScrollBar>
#include <QTimer>
#include <QJsonDocument>
#include <QJsonObject>

using namespace Core;
using namespace ProjectExplorer;
using namespace QmakeProjectManager;
using namespace QSsh;
using namespace RemoteLinux;
using namespace Utils;

namespace Nxt {
namespace Internal {

namespace {
const int CONNECTION_TEST_CHECK_FOR_CANCEL_INTERVAL = 2000;
}

class NxtConnectionTestStepConfigWidget : public SimpleBuildStepConfigWidget
{
    Q_OBJECT

public:
    NxtConnectionTestStepConfigWidget(NxtConnectionTestStep *step)
        : SimpleBuildStepConfigWidget(step)
    {
    }

    QString summaryText() const override
    {
        return QString::fromLatin1("<b>%1:</b> %2")
            .arg(displayName())
            .arg(tr("Verifies connection to the device can be established."));
    }
};

class NxtPrepareTargetStepConfigWidget : public SimpleBuildStepConfigWidget
{
    Q_OBJECT

public:
    NxtPrepareTargetStepConfigWidget(NxtPrepareTargetStep *step)
        : SimpleBuildStepConfigWidget(step)
    {
    }

    QString summaryText() const override
    {
        return QString::fromLatin1("<b>%1:</b> %2")
            .arg(displayName())
            .arg(tr("Prepares target device for deployment"));
    }
};

NxtProcessStep::NxtProcessStep(BuildStepList *bsl, Id id)
    :AbstractProcessStep(bsl,id)
{

}

bool NxtProcessStep::init(QList<const BuildStep *> &earlierSteps)
{
    return init(earlierSteps, NoInitOption);
}

const QString NxtProcessStep::vappFileName() const
{
    QmakeBuildConfiguration *bc = qobject_cast<QmakeBuildConfiguration*>(buildConfiguration());
    if (!bc) {
        bc = qobject_cast<QmakeBuildConfiguration*>(target()->activeBuildConfiguration());
    }

    const QString buildDirectory = bc->rawBuildDirectory().toString();
    const QString projectDirectory = project()->projectDirectory().toString();
    const QString manifestFilePath = [&]() -> QString {
            QString pathInProjectDirectory = Utils::FileName::fromString(projectDirectory).appendPath("manifest.json").toString();
            QFileInfo fi(pathInProjectDirectory);
            if (fi.exists() && fi.isFile()) {
                return pathInProjectDirectory;
            }

            QString pathInBuildDirectory = Utils::FileName::fromString(buildDirectory).appendPath("MANIFEST").toString();
            QFile file(pathInBuildDirectory);
            if (file.exists()) {
                file.open(QIODevice::ReadOnly | QIODevice::Text);
                const QString manifestFileName = QString::fromLocal8Bit(file.readAll()).simplified();
                file.close();
                return Utils::FileName::fromString(projectDirectory).appendPath(manifestFileName).toString();
            }

            return "";
    }();

    QFile manifest(manifestFilePath);
    if (manifest.exists()) {
        manifest.open(QIODevice::ReadOnly | QIODevice::Text);
        QJsonObject json = QJsonDocument::fromJson(manifest.readAll()).object();
        const QString vappFileName = QString("%1_%2.vapp").arg(json.value("Name").toString(), json.value("Version").toString());
        manifest.close();
        return vappFileName;
    }

    return "";
}

bool NxtProcessStep::init(QList<const BuildStep *> &earlierSteps, InitOptions options)
{
    QmakeBuildConfiguration *bc = qobject_cast<QmakeBuildConfiguration*>(buildConfiguration());
    if (!bc)
        bc =  qobject_cast<QmakeBuildConfiguration*>(target()->activeBuildConfiguration());

    if (!bc) {
        addOutput(tr("Cannot deploy: No active build configuration."), OutputFormat::ErrorMessage);
        return false;
    }

    const NxtSdk *const nxtSdk = NxtSdkKitInformation::sdk(target()->kit());

    if (!nxtSdk) {
        addOutput(tr("Cannot deploy: Missing IDS NXT Build Engine information in the kit"), OutputFormat::ErrorMessage);
        return false;
    }

    const QString target = NxtTargetKitInformation::targetName(this->target()->kit());

    if (target.isEmpty()) {
        addOutput(tr("Cannot deploy: Missing IDS NXT build-target information in the kit"), OutputFormat::ErrorMessage);
        return false;
    }

    IDevice::ConstPtr device = DeviceKitInformation::device(this->target()->kit());

    //TODO: HACK
    if (device.isNull() && !(options & DoNotNeedDevice)) {
        addOutput(tr("Cannot deploy: Missing IDS NXT device information in the kit"), OutputFormat::ErrorMessage);
        return false;
    }

    const QString projectDirectory = bc->isShadowBuild() ? bc->rawBuildDirectory().toString() : project()->projectDirectory().toString();
    const QString wrapperScriptsDir = NxtSdkManager::sdkToolsDirectory() + nxtSdk->virtualMachineName() + QLatin1Char('/') + target;
    const QString deployCommand = wrapperScriptsDir + QLatin1Char('/') + QLatin1String(Constants::NXT_WRAPPER_NXTDEPLOY);

    ProcessParameters *pp = processParameters();

    Environment env = bc ? bc->environment() : Environment::systemEnvironment();

    // By default, NXT_SSH_PROJECT_PATH is set to the project explorer variable "%{CurrentProject:Path}".
    // If multiple projects are open, "CurrentProject" may not be the project set as active (i.e. the
    // project being deployed). Instead, NXT_SSH_PROJECT_PATH needs to be set to the source directory of
    // the active project. Note that this path is the same, whether or not shadow builds are enabled.
    env.set(QLatin1String(Constants::NXT_SSH_PROJECT_PATH), project()->projectDirectory().toString());

    // TODO: HACK
    if(!device.isNull())
        env.appendOrSet(QLatin1String(Constants::NXT_SSH_DEVICE_NAME), device->displayName());
    pp->setMacroExpander(bc ? bc->macroExpander() : Utils::globalMacroExpander());
    pp->setEnvironment(env);
    pp->setWorkingDirectory(projectDirectory);
    pp->setCommand(deployCommand);
    pp->setArguments(QStringLiteral("--host %1 --username %2 --password \"%3\" %4").arg(
                         device.data()->sshParameters().host(),
                         device.data()->sshParameters().userName(),
                         device.data()->sshParameters().password(),
                         vappFileName()));
    return AbstractProcessStep::init(earlierSteps);
}

QString NxtProcessStep::arguments() const
{
    return m_arguments;
}

void NxtProcessStep::setArguments(const QString &arguments)
{
    m_arguments = arguments;
}

NxtDeployConfiguration *NxtProcessStep::deployConfiguration() const
{
    return qobject_cast<NxtDeployConfiguration *>(AbstractProcessStep::deployConfiguration());
}

///////////////////////////////////////////////////////////////////////////////////////////

Core::Id NxtConnectionTestStep::stepId()
{
    return Core::Id("QmakeProjectManager.NxtConnectionTestStep");
}

QString NxtConnectionTestStep::displayName()
{
    return tr("Test Device Connection");
}

NxtConnectionTestStep::NxtConnectionTestStep(BuildStepList *bsl)
    : BuildStep(bsl, stepId())
{
    setDefaultDisplayName(displayName());
}

bool NxtConnectionTestStep::init(QList<const BuildStep *> &earlierSteps)
{
    Q_UNUSED(earlierSteps);

    IDevice::ConstPtr d = DeviceKitInformation::device(this->target()->kit());
    if (!d) {
        setEnabled(false);
        return false;
    }

    return true;
}

void NxtConnectionTestStep::run(QFutureInterface<bool> &fi)
{
    IDevice::ConstPtr d = DeviceKitInformation::device(this->target()->kit());
    if (!d) {
        reportRunResult(fi, false);
        return;
    }

    m_futureInterface = &fi;

    RestConnectionParameters params(d->sshParameters());
#if 0 /* debug */
    params.setTimeout(10);
    params.setPort(RestConnectionParameters::ConnectionPortHttp);
    params.setHost(Constants::NXT_DEVICE_DEFAULT_HOST);
#endif /* debug */
    m_connection = new RestConnection(params, this);

#if 0 /* ssh */
    connect(m_connection, &RestConnection::connected, this, &NxtConnectionTestStep::onConnected);
#else
    // conntected: TCP socket connection established
    // authenticated: whatever HTTP request returned a status code >= 200
    connect(m_connection, &RestConnection::connected, m_connection, &RestConnection::authenticate);
    connect(m_connection, &RestConnection::authenticated, this, &NxtConnectionTestStep::onConnected);
#endif /* ssh */

    connect(m_connection, &RestConnection::error, this, &NxtConnectionTestStep::onConnectionFailure);

    emit addOutput(tr("%1: Testing connection to \"%2\"...").arg(displayName()).arg(d->displayName()), OutputFormat::NormalMessage);

    m_checkForCancelTimer = new QTimer(this);
    connect(m_checkForCancelTimer, &QTimer::timeout, this, &NxtConnectionTestStep::checkForCancel);
    m_checkForCancelTimer->start(CONNECTION_TEST_CHECK_FOR_CANCEL_INTERVAL);

    m_connection->connectToHost();

#if 0 /* ssh */
    m_connection = new SshConnection(d->sshParameters(), this);
    connect(m_connection, &SshConnection::connected, this, &NxtConnectionTestStep::onConnected);
    connect(m_connection, &SshConnection::error, this, &NxtConnectionTestStep::onConnectionFailure);

    emit addOutput(tr("%1: Testing connection to \"%2\"...").arg(displayName()).arg(d->displayName()), OutputFormat::NormalMessage);

    m_checkForCancelTimer = new QTimer(this);
    connect(m_checkForCancelTimer, &QTimer::timeout, this, &NxtConnectionTestStep::checkForCancel);
    m_checkForCancelTimer->start(CONNECTION_TEST_CHECK_FOR_CANCEL_INTERVAL);

    m_connection->connectToHost();
#endif /* ssh */
}

BuildStepConfigWidget *NxtConnectionTestStep::createConfigWidget()
{
    return new NxtConnectionTestStepConfigWidget(this);
}

bool NxtConnectionTestStep::immutable() const
{
    return false;
}

bool NxtConnectionTestStep::runInGuiThread() const
{
    return true;
}

void NxtConnectionTestStep::onAuthenticated()
{
    finish(true);
}

void NxtConnectionTestStep::onConnected()
{
    finish(true);
}

void NxtConnectionTestStep::onConnectionFailure()
{
    finish(false);
}

void NxtConnectionTestStep::checkForCancel()
{
    finish(false);
}

void NxtConnectionTestStep::finish(bool result)
{
    m_connection->disconnect(this);
    m_connection->deleteLater(), m_connection = 0;

    delete m_checkForCancelTimer, m_checkForCancelTimer = 0;
    reportRunResult(*m_futureInterface, result);
    m_futureInterface = 0;
}

///////////////////////////////////////////////////////////////////////////////////////////

/*!
 * \class NxtPrepareTargetStep
 * Wraps NxtConnectionTestStep based on IDevice::machineType
 */

Core::Id NxtPrepareTargetStep::stepId()
{
    return Core::Id("QmakeProjectManager.NxtPrepareTargetStep");
}

QString NxtPrepareTargetStep::displayName()
{
    return tr("Prepare Target");
}

NxtPrepareTargetStep::NxtPrepareTargetStep(BuildStepList *bsl)
    : BuildStep(bsl, stepId())
{
    setDefaultDisplayName(displayName());
    connect(&m_watcher, &QFutureWatcherBase::finished, this, &NxtPrepareTargetStep::onImplFinished);
}

bool NxtPrepareTargetStep::init(QList<const BuildStep *> &earlierSteps)
{
    IDevice::ConstPtr d = DeviceKitInformation::device(this->target()->kit());
    if (!d) {
        setEnabled(false);
        return false;
    }

    m_impl = new NxtConnectionTestStep(qobject_cast<BuildStepList *>(parent()));

    if (!m_impl->init(earlierSteps)) {
        delete m_impl;
        m_impl = nullptr;
        setEnabled(false);
        return false;
    }

    connect(m_impl, &BuildStep::addTask, this, &NxtPrepareTargetStep::addTask);
    connect(m_impl, &BuildStep::addOutput, this, &NxtPrepareTargetStep::addOutput);

    return true;
}

void NxtPrepareTargetStep::run(QFutureInterface<bool> &fi)
{
    m_watcher.setFuture(fi.future());
    m_impl->run(fi);
}

BuildStepConfigWidget *NxtPrepareTargetStep::createConfigWidget()
{
    return new NxtPrepareTargetStepConfigWidget(this);
}

bool NxtPrepareTargetStep::immutable() const
{
    return false;
}

bool NxtPrepareTargetStep::runInGuiThread() const
{
    return true;
}

void NxtPrepareTargetStep::onImplFinished()
{
    m_impl->disconnect(this);
    m_impl->deleteLater();
    m_impl = nullptr;
}

///////////////////////////////////////////////////////////////////////////////////////////

Core::Id NxtRestDeployStep::stepId()
{
    return Core::Id("QmakeProjectManager.NxtRestDeployStep");
}

QString NxtRestDeployStep::displayName()
{
    return QLatin1String("NXT Deploy");
}

NxtRestDeployStep::NxtRestDeployStep(BuildStepList *bsl)
    : NxtProcessStep(bsl, stepId())
{
    QmakeBuildConfiguration *bc = qobject_cast<QmakeBuildConfiguration*>(buildConfiguration());
    if (!bc)
        bc = qobject_cast<QmakeBuildConfiguration*>(target()->activeBuildConfiguration());

    if (bc)
    {
        buildDir = bc->rawBuildDirectory().toUserOutput();
        projectDir = bc->isShadowBuild() ? bc->rawBuildDirectory().toUserOutput() : project()->projectDirectory().toUserOutput();
    }

    setDefaultDisplayName(displayName());
    setArguments(arguments());
}

bool NxtRestDeployStep::init(QList<const BuildStep *> &earlierSteps)
{
    return NxtProcessStep::init(earlierSteps);
}

bool NxtRestDeployStep::immutable() const
{
    return false;
}

void NxtRestDeployStep::run(QFutureInterface<bool> &fi)
{
   emit addOutput(tr("Deploying VApp..."), OutputFormat::NormalMessage);
   AbstractProcessStep::run(fi);
}

BuildStepConfigWidget *NxtRestDeployStep::createConfigWidget()
{
    NxtDeployStepWidget *widget = new NxtDeployStepWidget(this);
    widget->setCommandText(QString("%1 in %2").arg(Constants::NXT_WRAPPER_NXTDEPLOY, buildDir));
    widget->setDisplayName(displayName());
    widget->setSummaryText(tr("Deploys VApp via REST interface."));
    return widget;
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

NxtDeployStepWidget::NxtDeployStepWidget(NxtProcessStep *step)
        : m_step(step)
{
    m_ui.setupUi(this);
    m_ui.commandArgumentsLineEdit->setText(m_step->arguments());
    connect(m_ui.commandArgumentsLineEdit, &QLineEdit::textEdited, this, &NxtDeployStepWidget::commandArgumentsLineEditTextEdited);
}

QString NxtDeployStepWidget::summaryText() const
{
    return QString::fromLatin1("<b>%1:</b> %2").arg(displayName()).arg(m_summaryText);
}

QString NxtDeployStepWidget::displayName() const
{
    return m_displayText;
}

void NxtDeployStepWidget::commandArgumentsLineEditTextEdited()
{
    m_step->setArguments(m_ui.commandArgumentsLineEdit->text());
}

void NxtDeployStepWidget::setCommandText(const QString& commandText)
{
     m_ui.commandLabelEdit->setText(commandText);
}

void NxtDeployStepWidget::setDisplayName(const QString& displayText)
{
    m_displayText = displayText;
}

void NxtDeployStepWidget::setSummaryText(const QString& summaryText)
{
    m_summaryText = summaryText;
}

QString NxtDeployStepWidget::commnadText() const
{
   return  m_ui.commandLabelEdit->text();
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

class NxtNamedCommandDeployService : public AbstractRemoteLinuxDeployService
{
    Q_OBJECT
    enum State { Inactive, Running };

public:
    explicit NxtNamedCommandDeployService(QObject *parent = nullptr);

    QString description() const { return m_description; }
    QString command() const { return m_command; }
    void setDescription(const QString &description) { m_description = description; }
    void setCommand(const QString &command) { m_command = command; }

    bool isDeploymentNecessary() const override { return true; }

protected:
    void doDeviceSetup() override { handleDeviceSetupDone(true); }
    void stopDeviceSetup() override { handleDeviceSetupDone(false); }
    void doDeploy() override;
    void stopDeployment() override;

private slots:
    void handleStdout();
    void handleStderr();
    void handleProcessClosed(int exitStatus);

private:
    State m_state{Inactive};
    QString m_description;
    QString m_command;
    SshRemoteProcessRunner *m_runner{};
};

NxtNamedCommandDeployService::NxtNamedCommandDeployService(QObject *parent)
    : AbstractRemoteLinuxDeployService(parent)
{
}

void NxtNamedCommandDeployService::doDeploy()
{
    QTC_CHECK(!m_description.isEmpty());
    QTC_CHECK(!m_command.isEmpty());
    QTC_ASSERT(m_state == Inactive, handleDeploymentDone());

    if (!m_runner)
        m_runner = new SshRemoteProcessRunner(this);
    connect(m_runner, &SshRemoteProcessRunner::readyReadStandardOutput, this, &NxtNamedCommandDeployService::handleStdout);
    connect(m_runner, &SshRemoteProcessRunner::readyReadStandardError, this, &NxtNamedCommandDeployService::handleStderr);
    connect(m_runner, &SshRemoteProcessRunner::processClosed, this, &NxtNamedCommandDeployService::handleProcessClosed);

    emit progressMessage(m_description);
    m_state = Running;
    m_runner->run(m_command.toUtf8(), deviceConfiguration()->sshParameters());
}

void NxtNamedCommandDeployService::stopDeployment()
{
    QTC_ASSERT(m_state == Running, return);

    disconnect(m_runner, 0, this, nullptr);
    m_runner->cancel();
    m_state = Inactive;
    handleDeploymentDone();
}

void NxtNamedCommandDeployService::handleStdout()
{
    emit stdOutData(QString::fromUtf8(m_runner->readAllStandardOutput()));
}

void NxtNamedCommandDeployService::handleStderr()
{
    emit stdErrData(QString::fromUtf8(m_runner->readAllStandardError()));
}

void NxtNamedCommandDeployService::handleProcessClosed(int exitStatus)
{
    QTC_ASSERT(m_state == Running, return);

    if (exitStatus == SshRemoteProcess::FailedToStart) {
        emit errorMessage(tr("Remote process failed to start."));
    } else if (exitStatus == SshRemoteProcess::CrashExit) {
        emit errorMessage(tr("Remote process was killed by a signal."));
    } else if (m_runner->processExitCode() != 0) {
        emit errorMessage(tr("Remote process finished with exit code %1.").arg(m_runner->processExitCode()));
    } else {
        emit progressMessage(tr("Remote command finished successfully."));
    }

    stopDeployment();
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

NxtNamedCommandDeployStep::NxtNamedCommandDeployStep(ProjectExplorer::BuildStepList *bsl, Core::Id id)
    : AbstractRemoteLinuxDeployStep(bsl, id)
    , m_deployService(new NxtNamedCommandDeployService(this))
{
}

AbstractRemoteLinuxDeployService *NxtNamedCommandDeployStep::deployService() const
{
    return m_deployService;
}

bool NxtNamedCommandDeployStep::initInternal(QString *error)
{
    return m_deployService->isDeploymentPossible(error);
}

void NxtNamedCommandDeployStep::setCommand(const QString &description, const QString &command)
{
    m_deployService->setDescription(description);
    m_deployService->setCommand(command);
}

} // namespace Internal
} // namespace Nxt

#include "nxtdeploysteps.moc"
