/****************************************************************************
**
** Copyright (C) 2012 - 2014 Jolla Ltd.
** Contact: http://jolla.com/
**
** This file is part of Qt Creator.
**
** 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.
**
** Other Usage
**
** Alternatively, this file may be used in accordance with the terms and
** conditions contained in a signed written agreement between you and Digia.
**
****************************************************************************/

#include "nxtoptionswidget.h"
#include "ui_nxtoptionswidget.h"

#include <vm/nxtconnection.h>
#include <vm/nxtconnectionmanager.h>
#include <nxtconstants.h>
#include <qtc/nxtsdkdetailswidget.h>
#include <qtc/nxtsdkmanager.h>
#include <qtc/nxtsdkselectiondialog.h>
#include <vm/nxtvirtualboxmanager.h>

#include <coreplugin/icore.h>
#include <ssh/sshconnection.h>
#include <utils/asconst.h>
#include <utils/fileutils.h>
#include <utils/qtcassert.h>

#include <QDesktopServices>
#include <QDir>
#include <QMessageBox>
#include <QStandardItem>
#include <QStandardItemModel>
#include <QUrl>

using namespace Core;
using namespace QSsh;
using namespace Utils;

namespace Nxt {
namespace Internal {

NxtOptionsWidget::NxtOptionsWidget(QWidget *parent)
    : QWidget(parent)
    , m_ui(new Ui::NxtOptionsWidget)
    , m_status(tr("Not connected."))
{
    m_ui->setupUi(this);
    connect(NxtSdkManager::instance(), &NxtSdkManager::sdksUpdated, this, &NxtOptionsWidget::onSdksUpdated);
    connect(m_ui->sdkDetailsWidget, &NxtSdkDetailsWidget::authorizeSshKey, this, &NxtOptionsWidget::onAuthorizeSshKey);
    connect(m_ui->sdkDetailsWidget, &NxtSdkDetailsWidget::generateSshKey, this, &NxtOptionsWidget::onGenerateSshKey);
    connect(m_ui->sdkDetailsWidget, &NxtSdkDetailsWidget::sshKeyChanged, this, &NxtOptionsWidget::onSshKeyChanged);
    connect(m_ui->sdkComboBox, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), this, &NxtOptionsWidget::onSdkChanged);
    connect(m_ui->addButton, &QPushButton::clicked, this, &NxtOptionsWidget::onAddButtonClicked);
    connect(m_ui->removeButton, &QPushButton::clicked, this, &NxtOptionsWidget::onRemoveButtonClicked);
    connect(m_ui->startVirtualMachineButton, &QPushButton::clicked, this, &NxtOptionsWidget::onStartVirtualMachineButtonClicked);
    connect(m_ui->stopVirtualMachineButton, &QPushButton::clicked, this, &NxtOptionsWidget::onStopVirtualMachineButtonClicked);
    connect(m_ui->sdkDetailsWidget, &NxtSdkDetailsWidget::testConnectionButtonClicked, this, &NxtOptionsWidget::onTestConnectionButtonClicked);
    connect(m_ui->sdkDetailsWidget, &NxtSdkDetailsWidget::sshTimeoutChanged, this, &NxtOptionsWidget::onSshTimeoutChanged);
    connect(m_ui->sdkDetailsWidget, &NxtSdkDetailsWidget::sshPortChanged, this, &NxtOptionsWidget::onSshPortChanged);
    connect(m_ui->sdkDetailsWidget, &NxtSdkDetailsWidget::headlessCheckBoxToggled, this, &NxtOptionsWidget::onHeadlessCheckBoxToggled);
    connect(m_ui->sdkDetailsWidget, &NxtSdkDetailsWidget::srcFolderApplyButtonClicked, this, &NxtOptionsWidget::onSrcFolderApplyButtonClicked);
    connect(m_ui->sdkDetailsWidget, &NxtSdkDetailsWidget::wwwPortChanged, this, &NxtOptionsWidget::onWwwPortChanged);
    onSdksUpdated();
}

NxtOptionsWidget::~NxtOptionsWidget()
{
    delete m_ui;

    // Destroy newly created but not-applied SDKs
    QSet<NxtSdk *> currentSdks = NxtSdkManager::sdks().toSet();
    foreach (NxtSdk *sdk, m_sdks) {
        if (!currentSdks.contains(sdk)) {
            delete sdk;
        }
    }
}

void NxtOptionsWidget::setSdk(const QString &vmName)
{
    if (m_sdks.contains(vmName))
        onSdkChanged(vmName);
}

QString NxtOptionsWidget::searchKeyWordMatchString() const
{
    const QChar blank = QLatin1Char(' ');
    QString keys;
    int count = m_ui->sdkComboBox->count();
    for (int i = 0; i < count; ++i)
        keys += m_ui->sdkComboBox->itemText(i) + blank;
    if (m_ui->sdkDetailsWidget->isVisible())
        keys += m_ui->sdkDetailsWidget->searchKeyWordMatchString();

    return keys.trimmed();
}

void NxtOptionsWidget::store()
{
    QMap<QString, NxtSdk*> sdks = m_sdks;
    QList<NxtSdk*> currentSdks = NxtSdkManager::sdks();

    bool ok = true;

    QList<NxtSdk*> lockedDownSdks;
    ok &= lockDownConnectionsOrCancelChangesThatNeedIt(&lockedDownSdks);

    foreach (NxtSdk *sdk, sdks) {
        if (m_sshPrivKeys.contains(sdk))
            sdk->setPrivateKeyFile(m_sshPrivKeys[sdk]);
        if (m_sshTimeout.contains(sdk))
            sdk->setTimeout(m_sshTimeout[sdk]);
        if (m_sshPort.contains(sdk)) {
            if (NxtVirtualBoxManager::updateSdkSshPort(sdk->virtualMachineName(), m_sshPort[sdk])) {
                sdk->setSshPort(m_sshPort[sdk]);
            } else {
                m_ui->sdkDetailsWidget->setSshPort(sdk->sshPort());
                m_sshPort.remove(sdk);
                ok = false;
            }
        }
        if (m_headless.contains(sdk))
            sdk->setHeadless(m_headless[sdk]);
        if (m_wwwPort.contains(sdk)) {
            if (NxtVirtualBoxManager::updateSdkWwwPort(sdk->virtualMachineName(), m_wwwPort[sdk])) {
                sdk->setWwwPort(m_wwwPort[sdk]);
            } else {
                m_ui->sdkDetailsWidget->setWwwPort(sdk->wwwPort());
                m_wwwPort.remove(sdk);
                ok = false;
            }
        }
    }

    foreach (NxtSdk *sdk, lockedDownSdks)
        sdk->connection()->lockDown(false);

    if (!ok) {
        QMessageBox::warning(this, tr("Some changes could not be saved!"),
                             tr("Failed to apply some of the changes to virtual machines"));
    }

    foreach (NxtSdk *sdk, currentSdks) {
        if (!sdks.contains(sdk->virtualMachineName())) {
            NxtSdkManager::removeSdk(sdk);
            delete sdk;
        } else {
            sdks.remove(sdk->virtualMachineName());
        }
    }

    foreach (NxtSdk *sdk,sdks)
        NxtSdkManager::addSdk(sdk);

    m_sshPrivKeys.clear();
    m_sshTimeout.clear();
    m_sshPort.clear();
    m_headless.clear();
    m_wwwPort.clear();
}

bool NxtOptionsWidget::lockDownConnectionsOrCancelChangesThatNeedIt(QList<NxtSdk *> *lockedDownSdks)
{
    QTC_ASSERT(lockedDownSdks, return false);

    QList<NxtSdk *> failed;

    for (NxtSdk *sdk : Utils::asConst(m_sdks)) {
        if (m_sshPort.value(sdk) == sdk->sshPort())
            m_sshPort.remove(sdk);
        if (m_wwwPort.value(sdk) == sdk->wwwPort())
            m_wwwPort.remove(sdk);

        if (!m_sshPort.contains(sdk) && !m_wwwPort.contains(sdk))
            continue;

        if (!sdk->connection()->isVirtualMachineOff()) {
            QPointer<QMessageBox> questionBox = new QMessageBox(QMessageBox::Question,
                    tr("Close Virtual Machine"),
                    tr("Close the \"%1\" virtual machine?").arg(m_virtualMachine),
                    QMessageBox::Yes | QMessageBox::No,
                    this);
            questionBox->setInformativeText(tr("Some of the changes require stopping the virtual machine before they can be applied."));
            if (questionBox->exec() != QMessageBox::Yes) {
                failed.append(sdk);
                continue;
            }
        }

        if (!sdk->connection()->lockDown(true)) {
            failed.append(sdk);
            continue;
        }

        lockedDownSdks->append(sdk);
    }

    for (NxtSdk *sdk : Utils::asConst(failed)) {
        m_ui->sdkDetailsWidget->setSshPort(sdk->sshPort());
        m_sshPort.remove(sdk);
        m_ui->sdkDetailsWidget->setWwwPort(sdk->wwwPort());
        m_wwwPort.remove(sdk);
    }

    return failed.isEmpty();
}

void NxtOptionsWidget::onSdkChanged(const QString &sdkName)
{
   if (m_virtualMachine != sdkName) {
       m_virtualMachine = sdkName;
       m_status = tr("Not tested.");
       update();
   }
}

void NxtOptionsWidget::onAddButtonClicked()
{
    NxtSdkSelectionDialog dialog(this);
    dialog.setWindowTitle(tr("Add a IDS NXT Build Engine"));
    if (dialog.exec() != QDialog::Accepted)
        return;

    if (m_sdks.contains(dialog.selectedSdkName()))
        return;

    NxtSdk* sdk = NxtSdkManager::createSdk(dialog.selectedSdkName());
    m_sdks[sdk->virtualMachineName()] = sdk;
    m_virtualMachine = sdk->virtualMachineName();
    update();
}

void NxtOptionsWidget::onRemoveButtonClicked()
{
    const QString &vmName = m_ui->sdkComboBox->itemData(m_ui->sdkComboBox->currentIndex(), Qt::DisplayRole).toString();
    if (vmName.isEmpty())
        return;

    if (m_sdks.contains(vmName)) {
         NxtSdk *removed = m_sdks.take(vmName);
         QList<NxtSdk *> currentSdks = NxtSdkManager::sdks();
         if (!currentSdks.contains(removed))
             delete removed;
         if (!m_sdks.isEmpty())
             m_virtualMachine = m_sdks.keys().last();
         else
             m_virtualMachine.clear();

         m_sshPrivKeys.remove(removed);
         m_sshTimeout.remove(removed);
         m_sshPort.remove(removed);
         m_headless.remove(removed);
         m_wwwPort.remove(removed);
    }
    update();
}

void NxtOptionsWidget::onTestConnectionButtonClicked()
{
    NxtSdk *sdk = m_sdks[m_virtualMachine];
    if (!sdk->connection()->isVirtualMachineOff()) {
        SshConnectionParameters params = sdk->connection()->sshParameters();
        if (m_sshPrivKeys.contains(sdk))
            params.privateKeyFile = m_sshPrivKeys[sdk];
        if (m_sshPort.contains(sdk))
            params.setPort(m_sshPort[sdk]);
        m_ui->sdkDetailsWidget->setStatus(tr("Connecting to machine %1 ...").arg(sdk->virtualMachineName()));
        m_ui->sdkDetailsWidget->setTestButtonEnabled(false);
        m_status = NxtConnectionManager::testConnection(params);
        m_ui->sdkDetailsWidget->setTestButtonEnabled(true);
        update();
    } else {
        m_ui->sdkDetailsWidget->setStatus(tr("Virtual machine %1 is not running.").arg(sdk->virtualMachineName()));
    }
}

void NxtOptionsWidget::onAuthorizeSshKey(const QString &file)
{
    NxtSdk *sdk = m_sdks[m_virtualMachine];
    const QString pubKeyPath = file + QLatin1String(".pub");
    const QString sshDirectoryPath = sdk->sharedSshPath() + QLatin1Char('/');
    const QStringList authorizedKeysPaths = QStringList()
            << sshDirectoryPath + QLatin1String("root/") + QLatin1String(Constants::NXT_AUTHORIZEDKEYS_FOLDER)
            << sshDirectoryPath + sdk->userName() + QLatin1Char('/') + QLatin1String(Constants::NXT_AUTHORIZEDKEYS_FOLDER);
    foreach (const QString &path, authorizedKeysPaths) {
        QString error;
        const bool success = NxtSdkManager::authorizePublicKey(path, pubKeyPath, error);
        if (!success)
            QMessageBox::critical(this, tr("Cannot Authorize Keys"), error);
        else
            QMessageBox::information(this, tr("Key Authorized"), tr("Key %1 added to \n %2").arg(pubKeyPath).arg(path));
    }
}

void NxtOptionsWidget::onStartVirtualMachineButtonClicked()
{
    const NxtSdk *sdk = m_sdks[m_virtualMachine];
    sdk->connection()->connectTo();
}

void NxtOptionsWidget::onStopVirtualMachineButtonClicked()
{
    const NxtSdk *sdk = m_sdks[m_virtualMachine];
    sdk->connection()->disconnectFrom();
}

void NxtOptionsWidget::onGenerateSshKey(const QString &privKeyPath)
{
    QString error;
    if (!NxtSdkManager::generateSshKey(privKeyPath, error)) {
       QMessageBox::critical(this, tr("Could not generate key."), error);
    } else {
       QMessageBox::information(this, tr("Key generated"), tr("Key pair generated \n %1 \n You should authorize key now.").arg(privKeyPath));
       m_ui->sdkDetailsWidget->setPrivateKeyFile(privKeyPath);
    }
}

void NxtOptionsWidget::onSdksUpdated()
{
    m_sdks.clear();
    m_virtualMachine.clear();
    foreach (NxtSdk *sdk, NxtSdkManager::sdks()) {
        m_sdks[sdk->virtualMachineName()] = sdk;
        m_virtualMachine = sdk->virtualMachineName();
    }
    update();
}

void NxtOptionsWidget::onSrcFolderApplyButtonClicked(const QString &newFolder)
{
    NxtSdk *sdk = m_sdks[m_virtualMachine];

    if (newFolder == sdk->sharedSrcPath()) {
        QMessageBox::information(this, tr("Choose a new folder"),
                                 tr("The given folder (%1) is the current alternative source folder. "
                                    "Please choose another folder if you want to change it.").arg(sdk->sharedSrcPath()));
        return;
    }

    if (!sdk->connection()->isVirtualMachineOff()) {
        QPointer<QMessageBox> questionBox = new QMessageBox(QMessageBox::Question,
                tr("Close Virtual Machine"),
                tr("Close the \"%1\" virtual machine?").arg(m_virtualMachine),
                QMessageBox::Yes | QMessageBox::No,
                this);
        questionBox->setInformativeText(tr("Virtual machine must be closed before the source folder can be changed."));
        if (questionBox->exec() != QMessageBox::Yes) {
            // reset the path in the chooser
            m_ui->sdkDetailsWidget->setSrcFolderChooserPath(sdk->sharedSrcPath());
            return;
        }
    }

    if (!sdk->connection()->lockDown(true)) {
        QMessageBox::warning(this, tr("Failed"),
                tr("Alternative source folder not changed"));
        // reset the path in the chooser
        m_ui->sdkDetailsWidget->setSrcFolderChooserPath(sdk->sharedSrcPath());
        return;
    }

    bool ok = NxtVirtualBoxManager::updateSharedFolder(m_virtualMachine,
            QLatin1String("src1"), newFolder);

    sdk->connection()->lockDown(false);

    if (ok) {
        // remember to update this value
        sdk->setSharedSrcPath(newFolder);

        const QMessageBox::StandardButton response =
            QMessageBox::question(this, tr("Success!"),
                                  tr("Alternative source folder for %1 changed to %2.\n\n"
                                     "Do you want to start %1 now?").arg(m_virtualMachine).arg(newFolder),
                                  QMessageBox::No | QMessageBox::Yes, QMessageBox::Yes);
        if (response == QMessageBox::Yes)
            sdk->connection()->connectTo();
    }
    else {
        QMessageBox::warning(this, tr("Changing the source folder failed!"),
                             tr("Unable to change the alternative source folder to %1").arg(newFolder));
        // reset the path in the chooser
        m_ui->sdkDetailsWidget->setSrcFolderChooserPath(sdk->sharedSrcPath());
    }
}

void NxtOptionsWidget::update()
{
    m_ui->sdkComboBox->clear();
    m_ui->sdkComboBox->addItems(m_sdks.keys());
    bool show = !m_virtualMachine.isEmpty();

    disconnect(m_vmOffConnection);

    if (show && m_sdks.contains(m_virtualMachine)) {
        NxtSdk *sdk = m_sdks[m_virtualMachine];
        m_ui->sdkDetailsWidget->setSdk(sdk);
        if (m_sshPrivKeys.contains(sdk))
            m_ui->sdkDetailsWidget->setPrivateKeyFile(m_sshPrivKeys[sdk]);
        else
            m_ui->sdkDetailsWidget->setPrivateKeyFile(sdk->privateKeyFile());

        if (m_sshTimeout.contains(sdk))
            m_ui->sdkDetailsWidget->setSshTimeout(m_sshTimeout[sdk]);
        else
            m_ui->sdkDetailsWidget->setSshTimeout(sdk->timeout());

        if (m_sshPort.contains(sdk))
            m_ui->sdkDetailsWidget->setSshPort(m_sshPort[sdk]);
        else
            m_ui->sdkDetailsWidget->setSshPort(sdk->sshPort());

        if (m_headless.contains(sdk))
            m_ui->sdkDetailsWidget->setHeadless(m_headless[sdk]);
        else
            m_ui->sdkDetailsWidget->setHeadless(sdk->isHeadless());

        if (m_wwwPort.contains(sdk))
            m_ui->sdkDetailsWidget->setWwwPort(m_wwwPort[sdk]);
        else
            m_ui->sdkDetailsWidget->setWwwPort(sdk->wwwPort());

        onVmOffChanged(sdk->connection()->isVirtualMachineOff());
        m_vmOffConnection = connect(sdk->connection(), &NxtConnection::virtualMachineOffChanged, this, &NxtOptionsWidget::onVmOffChanged);

        int index = m_ui->sdkComboBox->findText(m_virtualMachine);
        m_ui->sdkComboBox->setCurrentIndex(index);
        m_ui->sdkDetailsWidget->setStatus(m_status);
    }

    m_ui->sdkDetailsWidget->setVisible(show);
    m_ui->removeButton->setEnabled(show);
    m_ui->startVirtualMachineButton->setEnabled(show);
    m_ui->stopVirtualMachineButton->setEnabled(show);
}

void NxtOptionsWidget::onSshKeyChanged(const QString &file)
{
    //store keys to be saved on save click
    m_sshPrivKeys[m_sdks[m_virtualMachine]] = file;
}

void NxtOptionsWidget::onSshTimeoutChanged(int timeout)
{
    //store keys to be saved on save click
    m_sshTimeout[m_sdks[m_virtualMachine]] = timeout;
}

void NxtOptionsWidget::onSshPortChanged(quint16 port)
{
    //store keys to be saved on save click
    m_sshPort[m_sdks[m_virtualMachine]] = port;
}

void NxtOptionsWidget::onHeadlessCheckBoxToggled(bool checked)
{
    //store keys to be saved on save click
    m_headless[m_sdks[m_virtualMachine]] = checked;
}

void NxtOptionsWidget::onWwwPortChanged(quint16 port)
{
    //store keys to be saved on save click
    m_wwwPort[m_sdks[m_virtualMachine]] = port;
}

void NxtOptionsWidget::onVmOffChanged(bool vmOff)
{
    NxtSdk *sdk = m_sdks[m_virtualMachine];

    // If the VM is started, cancel any unsaved changes to SSH/WWW ports to prevent inconsistencies
    if (!vmOff) {
        m_ui->sdkDetailsWidget->setSshPort(sdk->sshPort());
        m_sshPort.remove(sdk);
        m_ui->sdkDetailsWidget->setWwwPort(sdk->wwwPort());
        m_wwwPort.remove(sdk);
    }

    m_ui->sdkDetailsWidget->setVmOffStatus(vmOff);
}

} // namespace Internal
} // namespace Nxt
