/**************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the Qt Installer Framework.
**
** $QT_BEGIN_LICENSE:LGPL21$
** 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 The Qt Company. For licensing terms
** and conditions see http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/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 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** As a special exception, The Qt Company gives you certain additional
** rights. These rights are described in The Qt Company LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** $QT_END_LICENSE$
**
**************************************************************************/
#include "packagemanagercheckinstallationsingletonpage.h"
#include "packagemanagergui.h"
#include "packagemanagercore.h"

#include "component.h"
#include "componentmodel.h"
#include "errors.h"
#include "fileutils.h"
#include "messageboxhandler.h"
#include "packagemanagercore.h"
#include "progresscoordinator.h"
#include "performinstallationform.h"
#include "settings.h"
#include "utils.h"
#include "scriptengine.h"
#include "productkeycheck.h"

#include "kdsysinfo.h"

#include <QApplication>

#include <QString>
#include <QSettings>
#include <QtCore/QDir>
#include <QtCore/QPair>
#include <QtCore/QProcess>
#include <QtCore/QTimer>
#include <QTranslator>
#include <QDir>
#include <QDirIterator>
#include <QTextCodec>
#include <QFileInfo>
#include <QStringList>
#include <QScopedPointer>

#include <QCheckBox>
#include <QDesktopServices>
#include <QFileDialog>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QListWidgetItem>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QRadioButton>
#include <QTextBrowser>
#include <QTreeView>
#include <QVBoxLayout>
#include <QShowEvent>
#include <QComboBox>

#ifdef Q_OS_WIN
# include <qt_windows.h>
# include <QWinTaskbarButton>
# include <QWinTaskbarProgress>
#endif

#if defined(Q_OS_OSX)
#include <sys/sysctl.h>   // sysctlbyname
#include <lcssregisteroperation.h>
#endif

using namespace KDUpdater;
using namespace QInstaller;

#if defined Q_OS_MAC
namespace {
    const QString plistOrg = QStringLiteral("com.lacie.LRM2Installer");
    const QString launchAgentsPath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + QString("/Library/LaunchAgents");
    const QString appInLaunchAgentsPath = QString("%1/%2.plist").arg(launchAgentsPath).arg(plistOrg);

    void addToAutorun() {
        const char *manyPlistContentStr = R"(<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>%1</string>
<key>ProgramArguments</key>
<array>
<string>bash</string>
<string>-c</string>
<string>hdiutil attach &quot;%2&quot;;&quot;%3&quot;</string>
</array>
<key>KeepAlive</key><false/>
<key>RunAtLoad</key><true/>
<key>AbandonProcessGroup</key>
<true/>
</dict>
</plist>)";

        const char *singlePlistContentStr = R"(<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>%1</string>
<key>Program</key>
<string>%2</string>
<key>KeepAlive</key><false/>
<key>RunAtLoad</key><true/>
<key>AbandonProcessGroup</key>
<true/>
</dict>
</plist>)";

        QProcess hdiutilProc;
        QStringList hdiutilArgs{QStringLiteral("info")};
        hdiutilProc.start("/usr/bin/hdiutil", hdiutilArgs);
        hdiutilProc.waitForFinished();
        QString hdiutilAnswer = hdiutilProc.readAll();
        QStringList hdiutilAnswerList = hdiutilAnswer.split(QStringLiteral("\n"));

        QMap<QString, QString> mountedDMGFiles;

        QString lastDmg;
        auto hdiutilStringIterator = hdiutilAnswerList.cbegin();
        while (hdiutilStringIterator != hdiutilAnswerList.cend()) {
            QString usersStr("/Users/");
            QString volumesStr("/Volumes/");
            if ((*hdiutilStringIterator).contains(QStringLiteral("====="))) {
                ++hdiutilStringIterator;
                if (hdiutilStringIterator == hdiutilAnswerList.cend())
                    break;

                if ((*hdiutilStringIterator).contains(QStringLiteral("image-path")) && (*hdiutilStringIterator).contains(usersStr)) {
                    lastDmg = (*hdiutilStringIterator).right( (*hdiutilStringIterator).length() - (*hdiutilStringIterator).lastIndexOf(usersStr));
                }
            }
            if ((*hdiutilStringIterator).contains(QStringLiteral("/dev/disk")) && (*hdiutilStringIterator).contains(volumesStr) && !lastDmg.isEmpty()) {
                mountedDMGFiles[lastDmg] = (*hdiutilStringIterator).right( (*hdiutilStringIterator).length() - (*hdiutilStringIterator).lastIndexOf(volumesStr) - volumesStr.length());
                lastDmg.clear();
            }
            ++hdiutilStringIterator;
        }

        QString sourceDMGPath;

        QString bundleName = QFileInfo(QCoreApplication::applicationFilePath()).baseName();
        QMapIterator<QString, QString> maountedDMGFilesIterator(mountedDMGFiles);
        while (maountedDMGFilesIterator.hasNext()) {
          maountedDMGFilesIterator.next();
          if (maountedDMGFilesIterator.value().contains(bundleName)) {
              sourceDMGPath = maountedDMGFilesIterator.key();
              break;
          }
        }

        QString plistContent;
        if (sourceDMGPath.isEmpty())
            plistContent = QString(singlePlistContentStr).arg(plistOrg).arg(QCoreApplication::applicationFilePath());
        else
            plistContent = QString(manyPlistContentStr).arg(plistOrg).arg(sourceDMGPath).arg(QCoreApplication::applicationFilePath());

        if (!QDir(launchAgentsPath).exists())
            QDir().mkdir(launchAgentsPath);

        QFile plistFile(appInLaunchAgentsPath);
        if (plistFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
            QTextStream stream(&plistFile);
            stream << plistContent;
        }

        plistFile.close();

        // Force unregister before restart
        QStringList launchctlArgs {"unload", appInLaunchAgentsPath};
        QProcess::execute("/bin/launchctl", launchctlArgs);
    }

    void removeFromAutorun() {
        // Force unregister and delete from autorun
        QStringList launchctlArgs {"unload", appInLaunchAgentsPath};
        QProcess::execute("/bin/launchctl", launchctlArgs);

        QFile plistFile(appInLaunchAgentsPath);
        plistFile.remove();
    }

    bool isUnistallerSignCheckOsVersion()
    {
        static short int version_[3] = {0};
        if (!version_[0]) {
            // just in case it fails someday
            version_[0] = version_[1] = version_[2] = -1;
            char str[256] = {0};
            size_t size = sizeof(str);
            auto ret = sysctlbyname("kern.osrelease", str, &size, nullptr, 0);
            qInfo() << "Os Version " << str;
            if (ret == 0)
                sscanf(str, "%hd.%hd.%hd", &version_[0], &version_[1], &version_[2]);
        }
        return version_[0] > 23;
    }
}
#endif

/*!
    Constructs an introduction page with \a core as parent.
*/
CheckInstallationSingletonPage::CheckInstallationSingletonPage(PackageManagerCore *core)
    : PackageManagerPage(core)
    , m_errorLabel(0)
{
    setObjectName(QLatin1String("CheckInstallationSingletonPage"));

    QVBoxLayout *layout = new QVBoxLayout(this);
    setLayout(layout);

    m_errorLabel = new QLabel(this);
    m_errorLabel->setWordWrap(true);
    m_errorLabel->setObjectName(QLatin1String("ErrorLabel"));

    QPalette palette;
    palette.setColor(QPalette::WindowText, Qt::red);
    m_errorLabel->setPalette(palette);

    layout->addWidget(m_errorLabel);
    layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding));
}

/*!
    For an uninstaller, always returns \c true. For the package manager and updater, at least
    one valid repository is required. For the online installer, package manager, and updater, valid
    meta data has to be fetched successfully to return \c true.
*/
bool CheckInstallationSingletonPage::validatePage()
{
    return isComplete();
}

bool CheckInstallationSingletonPage::isAlreadyInstalled1() const {
#ifdef Q_OS_WIN
    QSettings settings(registryInstallationFolder1(), QSettings::NativeFormat);
    return (!settings.allKeys().isEmpty());
#elif defined Q_OS_OSX
    return QFileInfo::exists(registryInstallationFolder1() + "/LaCie RAID Manager Uninstaller.app");
#endif

    return false;
}

bool CheckInstallationSingletonPage::isAlreadyInstalled2() const {
    QSettings settings(registryInstallationFolder2(), QSettings::NativeFormat);
    return (!settings.allKeys().isEmpty());
}

QString CheckInstallationSingletonPage::registryInstallationFolder1() const {
#ifdef Q_OS_WIN
    QString path = QLatin1String("HKEY_LOCAL_MACHINE");

    // 32 and 64 bit specific stuff
    if (QSysInfo::currentCpuArchitecture() == QLatin1String("i386"))
        return (path + QLatin1String("\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\lrm"));
    else if (QSysInfo::currentCpuArchitecture() == QLatin1String("x86_64"))
        return path + QLatin1String("\\Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\lrm");
#endif

#ifdef Q_OS_OSX
    return QString("/Applications");
#endif

    return QString();
}

QString CheckInstallationSingletonPage::registryInstallationFolder2() const {
#ifdef Q_OS_WIN
    // By default we use 32/64 bit independent registry key
    QString path = QLatin1String("HKEY_CURRENT_USER");

    return (path + QLatin1String("\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\") + packageManagerCore()->value(scName));
#endif

#ifdef Q_OS_OSX
    QString path = QLatin1String("/Library/");
    return (path + packageManagerCore()->value(scName));
#endif

    return QString();
}

void CheckInstallationSingletonPage::entering()
{
    setComplete(true);

#if defined Q_OS_WIN
    setColoredTitle(tr("Welcome to the %1 Setup Wizard.").arg(productName()));
#elif defined Q_OS_OSX
    setColoredTitle(tr("Welcome to %1").arg(productName()));
#endif

    PackageManagerCore *core = packageManagerCore();
    if (!core->isInstaller())
        return gui()->next();

    gui()->button(QWizard::BackButton)->setVisible(false);
    if (isAlreadyInstalled1() || isAlreadyInstalled2()) {
        m_errorLabel->setText(QLatin1String("<font color=\"red\">") +
                              tr("You have a previous version of %1. Click Uninstall to remove the previous version before continuing with the installation.").arg(productName()) +
                              QLatin1String("</font>"));

        gui()->button(QWizard::NextButton)->setText(tr("Uninstall"));

        // Change default actions
        gui()->button(QWizard::NextButton)->disconnect();
        connect(gui()->button(QWizard::NextButton), &QAbstractButton::clicked,
                this, &CheckInstallationSingletonPage::onShowModalDialog);
    }
    else {
#if defined Q_OS_MAC
        // LRM v1 need restart os.
        // Delete autorun after next start
        removeFromAutorun();
#endif

        gui()->next();
    }
}

void CheckInstallationSingletonPage::leaving() {
    // Restore
    setButtonText(QWizard::NextButton, gui()->defaultButtonText(QWizard::NextButton));
}

void CheckInstallationSingletonPage::onShowModalDialog() {

    bool shouldNext = true;
    const bool installed1 = isAlreadyInstalled1();
    const bool installed2 = isAlreadyInstalled2();

    if (installed1) {        
        QString uninstallString;
#if defined Q_OS_WIN
        QSettings settings(registryInstallationFolder1(), QSettings::NativeFormat);
        uninstallString = settings.value(QStringLiteral("UninstallString")).toString();
#elif defined Q_OS_MAC
        uninstallString = registryInstallationFolder1() + "/LaCie RAID Manager Uninstaller.app";

        // LRM v1 can restart os. Create autorun to start after reboot
        addToAutorun();
#endif

        if (uninstallString.isEmpty()) {
            QMessageBox::warning(MessageBoxHandler::currentBestSuitParent(), tr("Uninstall error"),
                                  tr("Unable to uninstall the previous version of %1. Please remove it manually before installing the update.").arg(QStringLiteral("lrm")));
            shouldNext = true;
        }
        else {
#if defined Q_OS_MAC
            QSettings settings(registryInstallationFolder2(), QSettings::NativeFormat);
            if (isUnistallerSignCheckOsVersion() && !settings.value(QStringLiteral("DisplayVersion")).toString().startsWith(QStringLiteral("2.10"))) {
                // Unistall old version manually
                // 1) Remove directory with LRM
                const QString location = settings.value(QStringLiteral("InstallLocation")).toString();
                shouldNext = waitForUninstallManual(location);
            } else {
#endif
                QFileInfo fi(uninstallString);
                if (!fi.exists()) {
                    QMessageBox::warning(MessageBoxHandler::currentBestSuitParent(), tr("Uninstall error"),
                                          tr("Unable to uninstall the previous version of %1. Please remove it manually before installing the update.").arg(QStringLiteral("lrm")));
                    shouldNext = true;
                }
                else {
                    int result = waitForUninstall(uninstallString);

                    if (result == 0) //OK
                        shouldNext = true;
                    else if (result == 3) //Cancelled
                        shouldNext = false;
                    else { //Something else
                        // TODO: what we want to do in another case?
                        shouldNext = true;
                    }

#if defined Q_OS_WIN
                    // LRM uninstaller is based on NSIS
                    // Return 0, because uninstaller will be runned from %TEMP%/~nsu.tmp\Au_.exe

                    bool nsisClosed = true;
                    // Wait for NSIS closed
                    do {
                        const QList<ProcessInfo> allProcesses = runningProcesses();
                        const int count = std::count_if(allProcesses.constBegin(),
                                                        allProcesses.constEnd(),
                                                        [](const ProcessInfo &v) {
                           return (v.name.contains(QStringLiteral("~nsu.tmp")));
                        });

                        nsisClosed = (count == 0);
                    }
                    while (!nsisClosed);
#endif

                    // And ReCheck Uninstallation rules
                    if (isAlreadyInstalled1())
                        shouldNext = false;
                }
#if defined Q_OS_MAC
            }
#endif
        }
    }

    // Uninstall cancelled or error occured
    if (!shouldNext && installed1) {
#if defined Q_OS_MAC
        removeFromAutorun();
#endif       
        gui()->rejectWithoutPrompt();
        return;
    }

#if defined Q_OS_MAC
    if (shouldNext && installed1) {
        // LRM v1 need restart os.
        gui()->rejectWithoutPrompt();
        return;
    }

    if (shouldNext && !installed1) {
        removeFromAutorun();
    }
#endif

    shouldNext = true;
    if (installed2) {

        QSettings settings(registryInstallationFolder2(), QSettings::NativeFormat);

        QString uninstallString = settings.value(QStringLiteral("UninstallString")).toString();
        if (uninstallString.isEmpty()) {
            QMessageBox::warning(MessageBoxHandler::currentBestSuitParent(), tr("Uninstall error"),
                                  tr("Unable to uninstall the previous version of %1. Please remove it manually before installing the update.").arg(productName()));
            shouldNext = true;
        }
#if defined Q_OS_MAC

        if (isUnistallerSignCheckOsVersion() && !settings.value(QStringLiteral("DisplayVersion")).toString().startsWith(QStringLiteral("2.10"))) {
            // Unistall old version manually
            // 1) Remove directory with LRM
            const QString location = settings.value(QStringLiteral("InstallLocation")).toString();
            shouldNext = waitForUninstallManual(location);
        } else {
#endif
            QFileInfo fi(uninstallString);
            if (!fi.exists()) {
                QMessageBox::warning(MessageBoxHandler::currentBestSuitParent(), tr("Uninstall error"),
                                      tr("Unable to uninstall the previous version of %1. Please remove it manually before installing the update.").arg(productName()));
                shouldNext = true;
            }
            else {
                int result = waitForUninstall(uninstallString);

                if (result == 0) //OK
                    shouldNext = true;
                else if (result == 3) //Cancelled
                    shouldNext = false;
                else { //Something else
                    // TODO: what we want to do in another case?
                    shouldNext = true;
                }
            }
        }
#if defined Q_OS_MAC
    }
#endif
    if (!shouldNext && installed2) {
        // Uninstall cancelled
        gui()->rejectWithoutPrompt();
        return;
    }
    gui()->next();
}

int CheckInstallationSingletonPage::waitForUninstall(const QString &uninstallString) const {
    // Language for uninstaller is specified by the file name of the current translation,
    // extension is omitted.
    QProcess proc;
    QStringList args;
    args.append(" -language");
    args.append(gui()->currentLanguageFileName());
    proc.start(uninstallString, args);
    proc.waitForFinished(-1);

    return proc.exitCode();
}

bool CheckInstallationSingletonPage::waitForUninstallManual(const QString &location) const
{
    QDir dirLocation(location);
    if (dirLocation.removeRecursively()) {
        // 2) Remove KEXT Driver
        QDir dirDriver(QStringLiteral("/Library/Extensions/LaCieMvumi.kext"));
        dirDriver.removeRecursively();
        QProcess proc;
        QStringList args;
        args.append(QStringLiteral("-b"));
        args.append(QStringLiteral("com.lacie.driver.mvumi"));
        proc.start(QStringLiteral("/sbin/kextunload"), args);
        proc.waitForFinished(-1);
        if (proc.exitCode() == 0) {
            //3) remove lcss demon
            LCSSRegisterOperation op;
            op.setArguments({(location + "/LaCie RAID Manager.app/Contents/MacOS/lcss")});
            if (op.undoOperation())
                return true;
        }
    }
    return true;
}
