/**************************************************************************
**
** 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 "packagemanagertargetdirectorypage.h"
#include "packagemanagergui.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

using namespace KDUpdater;
using namespace QInstaller;

// -- TargetDirectoryPage

/*!
    \class QInstaller::TargetDirectoryPage
    \inmodule QtInstallerFramework
    \brief The TargetDirectoryPage class specifies the target directory for the
    installation.

    End users can leave the page to continue the installation only if certain criteria are
    fulfilled. Some of them are checked in the validatePage() function, some in the
    targetDirWarning() function:

    \list
        \li No empty path given as target.
        \li No relative path given as target.
        \li Only ASCII characters are allowed in the path if the <AllowNonAsciiCharacters> element
            in the configuration file is set to \c false.
        \li The following ambiguous characters are not allowed in the path: [\"~<>|?*!@#$%^&:,;]
        \li No root or home directory given as target.
        \li On Windows, path names must be less than 260 characters long.
        \li No spaces in the path if the <AllowSpaceInPath> element in the configuration file is set
            to \c false.
    \endlist
*/

/*!
    Constructs a target directory selection page with \a core as parent.
*/
TargetDirectoryPage::TargetDirectoryPage(PackageManagerCore *core)
    : PackageManagerPage(core)
{
    setPixmap(QWizard::WatermarkPixmap, QPixmap());
    setObjectName(QLatin1String("TargetDirectoryPage"));

    QVBoxLayout *layout = new QVBoxLayout(this);
    layout->setContentsMargins(2,2,2,2);
    layout->setSpacing(2);

#ifdef Q_OS_WIN
    QLabel *folderIconLabel = new QLabel(this);
    folderIconLabel->setFixedSize(48,48);
    folderIconLabel->setScaledContents(true);;
    QPixmap mypix (QLatin1Literal(":/Folder.png"));
    folderIconLabel->setPixmap(mypix);
    layout->addWidget(folderIconLabel);
    layout->addItem(new QSpacerItem(5, 5, QSizePolicy::Fixed, QSizePolicy::Fixed));
#endif

    msgLabel = new QLabel(this);
    msgLabel->setWordWrap(true);
    msgLabel->setObjectName(QLatin1String("MessageLabel"));
    layout->addWidget(msgLabel);

    QHBoxLayout *hlayout = new QHBoxLayout;

    m_textChangeTimer.setSingleShot(true);
    m_textChangeTimer.setInterval(200);
    connect(&m_textChangeTimer, &QTimer::timeout, this, &QWizardPage::completeChanged);

    m_lineEdit = new QLineEdit(this);
    m_lineEdit->setObjectName(QLatin1String("TargetDirectoryLineEdit"));
    connect(m_lineEdit, &QLineEdit::textChanged,
            &m_textChangeTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
    hlayout->addWidget(m_lineEdit);

    browseButton = new QPushButton(this);
    browseButton->setObjectName(QLatin1String("BrowseDirectoryButton"));
    connect(browseButton, &QAbstractButton::clicked, this, &TargetDirectoryPage::dirRequested);
    browseButton->setShortcut(QKeySequence(tr("Alt+R", "browse file system to choose a file")));    
    hlayout->addWidget(browseButton);

    layout->addItem(new QSpacerItem(5, 5, QSizePolicy::Fixed, QSizePolicy::Fixed));
    layout->addLayout(hlayout);

    QPalette palette;
    palette.setColor(QPalette::WindowText, Qt::red);

    m_warningLabel = new QLabel(this);
    m_warningLabel->setPalette(palette);
    m_warningLabel->setWordWrap(true);
    m_warningLabel->setObjectName(QLatin1String("WarningLabel"));
    layout->addWidget(m_warningLabel);

    layout->addItem(new QSpacerItem(5, 5, QSizePolicy::Minimum, QSizePolicy::Expanding));

    m_diskSpaceLabel = new QLabel(this);
    m_diskSpaceLabel->setWordWrap(true);
    m_diskSpaceLabel->setObjectName(QLatin1String("DiskSpaceLabel"));
    layout->addWidget(m_diskSpaceLabel);

    setLayout(layout);
}

/*!
    Returns the target directory for the installation.
*/
QString TargetDirectoryPage::targetDir() const
{
    return m_lineEdit->text().trimmed();
}

/*!
    Sets the directory specified by \a dirName as the target directory for the
    installation.
*/
void TargetDirectoryPage::setTargetDir(const QString &dirName)
{
    m_lineEdit->setText(dirName);
}

/*!
    Initializes the page.
*/
void TargetDirectoryPage::initializePage()
{
    QString targetDir = packageManagerCore()->value(scTargetDir);
    if (targetDir.isEmpty()) {
        targetDir = QDir::homePath() + QDir::separator();
        if (!packageManagerCore()->settings().allowSpaceInPath()) {
            // prevent spaces in the default target directory
            if (targetDir.contains(QLatin1Char(' ')))
                targetDir = QDir::rootPath();
            targetDir += productName().remove(QLatin1Char(' '));
        } else {
            targetDir += productName();
        }
    }
    m_lineEdit->setText(QDir::toNativeSeparators(QDir(targetDir).absolutePath()));

    PackageManagerPage::initializePage();
}

/*!
    Checks whether the target directory exists and has contents:

    \list
        \li Returns \c true if the directory exists and is empty.
        \li Returns \c false if the directory already exists and contains an installation.
        \li Returns \c false if the target is a file or a symbolic link.
        \li Returns \c true or \c false if the directory exists but is not empty, depending on the
            choice that the end users make in the displayed message box.
    \endlist
*/
bool TargetDirectoryPage::validatePage()
{
    m_textChangeTimer.stop();

    if (!isComplete())
        return false;

    if (!isVisible())
        return true;

    const QString remove = packageManagerCore()->value(QLatin1String("RemoveTargetDir"));
    if (!QVariant(remove).toBool())
        return true;

    const QString targetDir = this->targetDir();
    const QDir dir(targetDir);
    // the directory exists and is empty...
    if (dir.exists() && dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).isEmpty())
        return true;

    const QFileInfo fi(targetDir);
    if (fi.isDir()) {
        QString fileName = packageManagerCore()->settings().maintenanceToolName();
#if defined(Q_OS_OSX)
        if (QInstaller::isInBundle(QCoreApplication::applicationDirPath()))
            fileName += QLatin1String(".app/Contents/MacOS/") + fileName;
#elif defined(Q_OS_WIN)
        fileName += QLatin1String(".exe");
#endif

        QFileInfo fi2(targetDir + QDir::separator() + fileName);
        if (fi2.exists()) {
            return failWithError(QLatin1String("TargetDirectoryInUse"), tr("The folder you selected already exists and contains an installation. Choose a different target for the installation."));
        }

        return askQuestion(QLatin1String("OverwriteTargetDirectory"),
            tr("You have selected a folder that contains files. Please consider choosing another folder due to the strong risk that the uninstaller can fail and, even if it succeeds, all current files will be deleted. Do you want to continue?"));
    } else if (fi.isFile() || fi.isSymLink()) {
        return failWithError(QLatin1String("WrongTargetDirectory"), tr("The selected file or directory already exists. Please choose another destination for the installation."));
    }
    return true;
}

/*!
    Initializes the page's fields based on values from fields on previous
    pages.
*/
void TargetDirectoryPage::entering()
{
    browseButton->setText(tr("B&rowse..."));
    msgLabel->setText(tr("To continue, click Next. To select a different folder, click Browse."));

    if (QPushButton *const b = qobject_cast<QPushButton *>(gui()->button(QWizard::NextButton)))
        b->setDefault(true);

#if (defined Q_OS_WIN) || (defined Q_OS_OSX)
    PackageManagerCore *core = packageManagerCore();
    if (core->isInstaller()) {
        if (QWizard *w = wizard()) {
             w->setOption(QWizard::IgnoreSubTitles, false);

            // Restore default actions
            w->button(QWizard::NextButton)->disconnect();
            connect(w->button(QWizard::NextButton), &QAbstractButton::clicked,
                    w, &QWizard::next);
        }
    }
#endif

#ifdef Q_OS_OSX
    if (PackageManagerGui *w = gui()) {
        w->button(QWizard::BackButton)->disconnect();
        connect(w->button(QWizard::BackButton), &QAbstractButton::clicked,
                w, &QWizard::back);
    }
#endif

    setComplete(false);

    Q_ASSERT(packageManagerCore()->isInstaller());

    setButtonText(QWizard::NextButton, tr("&Install"));
    setColoredTitle(tr("Select Destination"));
#ifdef Q_OS_WIN
    setColoredSubTitle(tr("Choose the location to install %1.").arg(productName()));
#endif

    const VolumeInfo tempVolume = VolumeInfo::fromPath(QDir::tempPath());
    const VolumeInfo targetVolume = VolumeInfo::fromPath(packageManagerCore()->value(scTargetDir));

    const quint64 tempVolumeAvailableSize = tempVolume.availableSize();
    const quint64 installVolumeAvailableSize = targetVolume.availableSize();

    // at the moment there is no better way to check this
    if (targetVolume.size() == 0 && installVolumeAvailableSize == 0) {
        qDebug().nospace() << "Cannot determine available space on device. "
                              "Volume descriptor: " << targetVolume.volumeDescriptor()
                           << ", Mount path: " << targetVolume.mountPath() << ". Continue silently.";
        return;     // TODO: Shouldn't this also disable the "Next" button?
    }

    const bool tempOnSameVolume = (targetVolume == tempVolume);
    if (tempOnSameVolume) {
        qDebug() << "Tmp and install directories are on the same volume. Volume mount point:"
            << targetVolume.mountPath() << "Free space available:"
            << humanReadableSize(installVolumeAvailableSize);
    } else {
        qDebug() << "Tmp is on a different volume than the installation directory. Tmp volume mount point:"
            << tempVolume.mountPath() << "Free space available:"
            << humanReadableSize(tempVolumeAvailableSize) << "Install volume mount point:"
            << targetVolume.mountPath() << "Free space available:"
            << humanReadableSize(installVolumeAvailableSize);
    }

    const quint64 extraSpace = 256 * 1024 * 1024LL;
    quint64 required(packageManagerCore()->requiredDiskSpace());
    quint64 tempRequired(packageManagerCore()->requiredTemporaryDiskSpace());
    if (required < extraSpace) {
        required += 0.1 * required;
        tempRequired += 0.1 * tempRequired;
    } else {
        required += extraSpace;
        tempRequired += extraSpace;
    }

    quint64 repositorySize = 0;
    const bool createLocalRepository = packageManagerCore()->createLocalRepositoryFromBinary();
    if (createLocalRepository && packageManagerCore()->isInstaller()) {
        repositorySize = QFile(QCoreApplication::applicationFilePath()).size();
        // if we create a local repository, take that space into account as well
        required += repositorySize;
    }

    qDebug() << "Installation space required:" << humanReadableSize(required) << "Temporary space "
        "required:" << humanReadableSize(tempRequired) << "Local repository size:"
        << humanReadableSize(repositorySize);

    if (tempOnSameVolume && (installVolumeAvailableSize <= (required + tempRequired))) {
        m_diskSpaceLabel->setText(tr("Insufficient space for the temporary and installer files. Available: %1, Required: %2")
            .arg(humanReadableSize(installVolumeAvailableSize),
            humanReadableSize(required + tempRequired)));
        setComplete(false);
        return;
    }

    if (installVolumeAvailableSize < required) {
        m_diskSpaceLabel->setText(tr("Insufficient space for the selected app components. Available: %1, Required: %2").arg(humanReadableSize(installVolumeAvailableSize),
            humanReadableSize(required)));
        setComplete(false);
        return;
    }

    if (tempVolumeAvailableSize < tempRequired) {
        m_diskSpaceLabel->setText(tr("Insufficient space for the temporary files. Available: %1, Required: %2").arg(humanReadableSize(tempVolumeAvailableSize),
            humanReadableSize(tempRequired)));
        setComplete(false);
        return;
    }

    if (installVolumeAvailableSize - required < 0.01 * targetVolume.size()) {
        // warn for less than 1% of the volume's space being free
        m_diskSpaceLabel->setText(tr("The disk you selected appears to have sufficient space. However, there will be less than 1% storage space available after the installation. %1"));
    } else if (installVolumeAvailableSize - required < 100 * 1024 * 1024LL) {
        // warn for less than 100MB being free
        m_diskSpaceLabel->setText(tr("The disk you selected appears to have sufficient space. However, there will be less than 100MB storage space available after the installation. %1"));
    }

    m_diskSpaceLabel->setText(QString::fromLatin1("%1").arg(tr("A minimum of %1 of free storage space is required.")
            .arg(humanReadableSize(packageManagerCore()->requiredDiskSpace()))));
}

/*!
    Called when end users leave the page and the PackageManagerGui:currentPageChanged()
    signal is triggered.
*/
void TargetDirectoryPage::leaving()
{
    packageManagerCore()->setValue(scTargetDir, targetDir());
}

void TargetDirectoryPage::dirRequested()
{
    const QString newDirName = QFileDialog::getExistingDirectory(this,
        tr("Select Installer"), targetDir());
    if (newDirName.isEmpty() || newDirName == targetDir())
        return;
    m_lineEdit->setText(QDir::toNativeSeparators(newDirName));
}

/*!
    Requests a warning message to be shown to end users upon invalid input. If the input is valid,
    the \uicontrol Next button is enabled.

    Returns \c true if a valid path to the target directory is set; otherwise returns \c false.
*/
bool TargetDirectoryPage::isComplete() const
{
    m_warningLabel->setText(targetDirWarning());
    m_warningLabel->setVisible(!m_warningLabel->text().isEmpty());
    return m_warningLabel->text().isEmpty();
}

/*!
    Returns a warning if the path to the target directory is not set or if it
    is invalid. Installation can continue only after a valid target path is given.
*/
QString TargetDirectoryPage::targetDirWarning() const
{
    if (targetDir().isEmpty())
        return tr("Please choose a folder for the installation.");

    QDir target(targetDir());
    if (target.isRelative())
        return tr("Please choose a specific folder for the installation.");

    QString nativeTargetDir = QDir::toNativeSeparators(target.absolutePath());
    if (!packageManagerCore()->settings().allowNonAsciiCharacters()) {
        for (int i = 0; i < nativeTargetDir.length(); ++i) {
            if (nativeTargetDir.at(i).unicode() & 0xff80) {
                return tr("The folder name contains unsupported characters. Please choose a different folder.");
            }
        }
    }

    target = target.canonicalPath();
    if (target == QDir::root() || target == QDir::home()) {
        return tr("The folder for the installation has been deleted. Please choose a different folder.")
            .arg(QDir::toNativeSeparators(target.path()));
    }

#ifdef Q_OS_WIN
    // folder length (set by user) + maintenance tool name length (no extension) + extra padding
    if ((nativeTargetDir.length()
        + packageManagerCore()->settings().maintenanceToolName().length() + 20) >= MAX_PATH) {
        return tr("The path to the folder is too long. Please choose a different folder.");
    }

    static QRegularExpression reg(QLatin1String(
        "^(?<drive>[a-zA-Z]:\\\\)|"
        "^(\\\\\\\\(?<path>\\w+)\\\\)|"
        "^(\\\\\\\\(?<ip>\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\\\)"));
    const QRegularExpressionMatch regMatch = reg.match(nativeTargetDir);

    const QString ipMatch = regMatch.captured(QLatin1String("ip"));
    const QString pathMatch = regMatch.captured(QLatin1String("path"));
    const QString driveMatch = regMatch.captured(QLatin1String("drive"));

    if (ipMatch.isEmpty() && pathMatch.isEmpty() && driveMatch.isEmpty()) {
        return tr("The path to the folder is not valid. Please choose a different disk.");
    }

    if (!driveMatch.isEmpty()) {
        bool validDrive = false;
        const QFileInfo drive(driveMatch);
        foreach (const QFileInfo &driveInfo, QDir::drives()) {
            if (drive == driveInfo) {
                validDrive = true;
                break;
            }
        }
        if (!validDrive) {  // right now we can only verify local drives
            return tr("The path to the folder is not valid. Please choose a different folder.");
        }
        nativeTargetDir = nativeTargetDir.mid(2);
    }

    if (nativeTargetDir.endsWith(QLatin1Char('.')))
        return tr("The installation path cannot end with '.'. Please choose a different folder.");

    QString ambiguousChars = QLatin1String("[\"~<>|?*!@#$%^&:,; ]"
        "|(\\\\CON)(\\\\|$)|(\\\\PRN)(\\\\|$)|(\\\\AUX)(\\\\|$)|(\\\\NUL)(\\\\|$)|(\\\\COM\\d)(\\\\|$)|(\\\\LPT\\d)(\\\\|$)");
#else // Q_OS_WIN
    QString ambiguousChars = QStringLiteral("[~<>|?*!@#$%^&:,; \\\\]");
#endif // Q_OS_WIN

    if (packageManagerCore()->settings().allowSpaceInPath())
        ambiguousChars.remove(QLatin1Char(' '));

    static QRegularExpression ambCharRegEx(ambiguousChars, QRegularExpression::CaseInsensitiveOption);
    // check if there are not allowed characters in the target path
    QRegularExpressionMatch match = ambCharRegEx.match(nativeTargetDir);
    if (match.hasMatch()) {
        return tr("The installation path cannot include %1. Please choose a different folder.").arg(match.captured(0));
    }

    return QString();
}

/*!
    Returns \c true if a warning message specified by \a message with the
    identifier \a identifier is presented to end users for acknowledgment.
*/
bool TargetDirectoryPage::askQuestion(const QString &identifier, const QString &message)
{
    QMessageBox::StandardButton bt =
        MessageBoxHandler::warning(MessageBoxHandler::currentBestSuitParent(), identifier,
        tr("Warning"), message, QMessageBox::Yes | QMessageBox::No);
    return bt == QMessageBox::Yes;
}

bool TargetDirectoryPage::failWithError(const QString &identifier, const QString &message)
{
    MessageBoxHandler::critical(MessageBoxHandler::currentBestSuitParent(), identifier,
        tr("Error"), message);
    return false;
}

//#include "packagemanagertargetdirectorypage.moc"
//#include "moc_packagemanagertargetdirectorypage.cpp"
