/****************************************************************************
**
** Copyright (C) 2013 Klaralvdalens Datakonsult AB (KDAB)
** Contact: http://www.qt-project.org/legal
**
** This file is part of the Qt Installer Framework.
**
** $QT_BEGIN_LICENSE:LGPL$
** 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.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3.0 as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU General Public License version 3.0 requirements will be
** met: http://www.gnu.org/copyleft/gpl.html.
**
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "kdupdaterupdateoperations.h"
#include "errors.h"
#include "fileutils.h"

#include <QDir>
#include <QFile>
#include <QTextStream>
#include <QTemporaryFile>
#include <QFileInfo>

#include <cerrno>

using namespace KDUpdater;

static QString errnoToQString(int error)
{
#if defined(Q_OS_WIN) && !defined(Q_CC_MINGW)
    char msg[128];
    if (strerror_s(msg, sizeof msg, error) != 0)
        return QString::fromLocal8Bit(msg);
    return QString();
#else
    return QString::fromLocal8Bit(strerror(error));
#endif
}

static bool removeDirectory(const QString &path, QString *errorString, bool force = true)
{
    Q_ASSERT(errorString);
    const QFileInfoList entries = QDir(path).entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries | QDir::Hidden);
    for (QFileInfoList::const_iterator it = entries.constBegin(); it != entries.constEnd(); ++it) {
        if (it->isDir() && !it->isSymLink()) {
            removeDirectory(it->filePath(), errorString, force);
        } else if (force) {
            QFile f(it->filePath());
            if (!f.remove())
                return false;
        }
    }

    // even remove some hidden, OS-created files in there
#if defined Q_OS_MAC
    QFile::remove(path + QLatin1String("/.DS_Store"));
#elif defined Q_OS_WIN
    QFile::remove(path + QLatin1String("/Thumbs.db"));
#endif

    errno = 0;
    const bool success = QDir().rmdir(path);
    if (errno)
        *errorString = errnoToQString(errno);
    return success;
}

/*
 * \internal
 * Returns a filename for a temporary file based on \a templateName
 */
static QString backupFileName(const QString &templateName)
{
    QTemporaryFile file(templateName);
    file.open();
    const QString name = file.fileName();
    file.close();
    file.remove();
    return name;
}

////////////////////////////////////////////////////////////////////////////
// KDUpdater::CopyOperation
////////////////////////////////////////////////////////////////////////////

CopyOperation::CopyOperation()
{
    setName(QLatin1String("Copy"));
}

CopyOperation::~CopyOperation()
{
    deleteFileNowOrLater(value(QLatin1String("backupOfExistingDestination")).toString());
}

QString CopyOperation::sourcePath()
{
    return arguments().first();
}

QString CopyOperation::destinationPath()
{
    QString destination = arguments().last();

    // if the target is a directory use the source filename to complete the destination path
    if (QFileInfo(destination).isDir())
        destination = QDir(destination).filePath(QFileInfo(sourcePath()).fileName());
    return destination;
}


void CopyOperation::backup()
{
    QString destination = destinationPath();

    if (!QFile::exists(destination)) {
        clearValue(QLatin1String("backupOfExistingDestination"));
        return;
    }

    setValue(QLatin1String("backupOfExistingDestination"), backupFileName(destination));

    // race condition: The backup file could get created by another process right now. But this is the same
    // in QFile::copy...
    if (!QFile::rename(destination, value(QLatin1String("backupOfExistingDestination")).toString()))
        setError(UserDefinedError, tr("Could not backup file %1.").arg(destination));
}

bool CopyOperation::performOperation()
{
    // We need two args to complete the copy operation. First arg provides the complete file name of source
    // Second arg provides the complete file name of dest
    if (arguments().count() != 2) {
        setError(InvalidArguments);
        setErrorString(tr("Invalid arguments: %1 arguments given, 2 expected.").arg(arguments().count()));
        return false;
    }

    QString source = sourcePath();
    QString destination = destinationPath();

    QFile sourceFile(source);
    if (!sourceFile.exists()) {
        setError(UserDefinedError);
        setErrorString(tr("Could not copy a none existing file: %1").arg(source));
        return false;
    }
    // If destination file exists, we cannot use QFile::copy() because it does not overwrite an existing
    // file. So we remove the destination file.
    QFile destinationFile(destination);
    if (destinationFile.exists()) {
        if (!destinationFile.remove()) {
            setError(UserDefinedError);
            setErrorString(tr("Could not remove destination file %1: %2").arg(destination, destinationFile.errorString()));
            return false;
        }
    }

    const bool copied = sourceFile.copy(destination);
    if (!copied) {
        setError(UserDefinedError);
        setErrorString(tr("Could not copy %1 to %2: %3").arg(source, destination, sourceFile.errorString()));
    }
    return copied;
}

bool CopyOperation::undoOperation()
{
    QString source = sourcePath();
    QString destination = destinationPath();

    // if the target is a directory use the source filename to complete the destination path
    if (QFileInfo(destination).isDir())
        destination = destination + QDir::separator() + QFileInfo(source).fileName();

    QFile destFile(destination);
    // first remove the dest
    if (destFile.exists() && !destFile.remove()) {
        setError(UserDefinedError, tr("Could not delete file %1: %2").arg(destination, destFile.errorString()));
        return false;
    }

    // no backup was done:
    // the copy destination file wasn't existing yet - that's no error
    if (!hasValue(QLatin1String("backupOfExistingDestination")))
        return true;

    QFile backupFile(value(QLatin1String("backupOfExistingDestination")).toString());
    // otherwise we have to copy the backup back:
    const bool success = backupFile.rename(destination);
    if (!success)
        setError(UserDefinedError, tr("Could not restore backup file into %1: %2").arg(destination, backupFile.errorString()));
    return success;
}

/*!
 \reimp
 */
QDomDocument CopyOperation::toXml() const
{
    // we don't want to save the backupOfExistingDestination
    if (!hasValue(QLatin1String("backupOfExistingDestination")))
        return UpdateOperation::toXml();

    CopyOperation *const me = const_cast<CopyOperation *>(this);

    const QVariant v = value(QLatin1String("backupOfExistingDestination"));
    me->clearValue(QLatin1String("backupOfExistingDestination"));
    const QDomDocument xml = UpdateOperation::toXml();
    me->setValue(QLatin1String("backupOfExistingDestination"), v);
    return xml;
}

bool CopyOperation::testOperation()
{
    // TODO
    return true;
}

CopyOperation *CopyOperation::clone() const
{
    return new CopyOperation();
}


////////////////////////////////////////////////////////////////////////////
// KDUpdater::MoveOperation
////////////////////////////////////////////////////////////////////////////

MoveOperation::MoveOperation()
{
    setName(QLatin1String("Move"));
}

MoveOperation::~MoveOperation()
{
    deleteFileNowOrLater(value(QLatin1String("backupOfExistingDestination")).toString());
}

void MoveOperation::backup()
{
    const QString dest = arguments().last();
    if (!QFile::exists(dest)) {
        clearValue(QLatin1String("backupOfExistingDestination"));
        return;
    }

    setValue(QLatin1String("backupOfExistingDestination"), backupFileName(dest));

    // race condition: The backup file could get created by another process right now. But this is the same
    // in QFile::copy...
    if (!QFile::rename(dest, value(QLatin1String("backupOfExistingDestination")).toString()))
        setError(UserDefinedError, tr("Could not backup file %1.").arg(dest));
}

bool MoveOperation::performOperation()
{
    // We need two args to complete the copy operation. // First arg provides the complete file name of
    // source, second arg provides the complete file name of dest
    const QStringList args = this->arguments();
    if (args.count() != 2) {
        setError(InvalidArguments);
        setErrorString(tr("Invalid arguments: %1 arguments given, 2 expected.").arg(args.count()));
        return false;
    }

    const QString dest = args.last();
    // If destination file exists, then we cannot use QFile::copy() because it does not overwrite an existing
    // file. So we remove the destination file.
    if (QFile::exists(dest)) {
        QFile file(dest);
        if (!file.remove(dest)) {
            setError(UserDefinedError);
            setErrorString(tr("Could not remove destination file %1: %2").arg(dest, file.errorString()));
            return false;
        }
    }

    // Copy source to destination.
    QFile file(args.first());
    if (!file.copy(dest)) {
        setError(UserDefinedError);
        setErrorString(tr("Could not copy %1 to %2: %3").arg(file.fileName(), dest, file.errorString()));
        return false;
    }
    return deleteFileNowOrLater(file.fileName());
}

bool MoveOperation::undoOperation()
{
    const QStringList args = arguments();
    const QString dest = args.last();
    // first: copy back the destination to source
    QFile destF(dest);
    if (!destF.copy(args.first())) {
        setError(UserDefinedError, tr("Cannot copy %1 to %2: %3").arg(dest, args.first(), destF.errorString()));
        return false;
    }

    // second: delete the move destination
    if (!deleteFileNowOrLater(dest)) {
        setError(UserDefinedError, tr("Cannot remove file %1."));
        return false;
    }

    // no backup was done:
    // the move destination file wasn't existing yet - that's no error
    if (!hasValue(QLatin1String("backupOfExistingDestination")))
        return true;

    // otherwise we have to copy the backup back:
    QFile backupF(value(QLatin1String("backupOfExistingDestination")).toString());
    const bool success = backupF.rename(dest);
    if (!success)
        setError(UserDefinedError, tr("Cannot restore backup file for %1: %2").arg(dest, backupF.errorString()));

    return success;
}

bool MoveOperation::testOperation()
{
    // TODO
    return true;
}

MoveOperation *MoveOperation::clone() const
{
    return new MoveOperation;
}


////////////////////////////////////////////////////////////////////////////
// KDUpdater::DeleteOperation
////////////////////////////////////////////////////////////////////////////

DeleteOperation::DeleteOperation()
{
    setName(QLatin1String("Delete"));
}

DeleteOperation::~DeleteOperation()
{
    deleteFileNowOrLater(value(QLatin1String("backupOfExistingFile")).toString());
}

void DeleteOperation::backup()
{
    const QString fileName = arguments().first();
    setValue(QLatin1String("backupOfExistingFile"), backupFileName(fileName));

    QFile file(fileName);
    if (!file.copy(value(QLatin1String("backupOfExistingFile")).toString()))
        setError(UserDefinedError, tr("Cannot create backup of %1: %2").arg(fileName, file.errorString()));
}

bool DeleteOperation::performOperation()
{
    // Requires only one parameter. That is the name of the file to remove.
    const QStringList args = this->arguments();
    if (args.count() != 1) {
        setError(InvalidArguments);
        setErrorString(tr("Invalid arguments: %1 arguments given, 1 expected.").arg(args.count()));
        return false;
    }
    return deleteFileNowOrLater(args.first());
}

bool DeleteOperation::undoOperation()
{
    if (!hasValue(QLatin1String("backupOfExistingFile")))
        return true;

    const QString fileName = arguments().first();
    QFile backupF(value(QLatin1String("backupOfExistingFile")).toString());
    const bool success = backupF.copy(fileName) && deleteFileNowOrLater(backupF.fileName());
    if (!success)
        setError(UserDefinedError, tr("Cannot restore backup file for %1: %2").arg(fileName, backupF.errorString()));
    return success;
}

bool DeleteOperation::testOperation()
{
    // TODO
    return true;
}

DeleteOperation *DeleteOperation::clone() const
{
    return new DeleteOperation;
}

/*!
 \reimp
 */
QDomDocument DeleteOperation::toXml() const
{
    // we don't want to save the backupOfExistingFile
    if (!hasValue(QLatin1String("backupOfExistingFile")))
        return UpdateOperation::toXml();

    DeleteOperation *const me = const_cast<DeleteOperation *>(this);

    const QVariant v = value(QLatin1String("backupOfExistingFile"));
    me->clearValue(QLatin1String("backupOfExistingFile"));
    const QDomDocument xml = UpdateOperation::toXml();
    me->setValue(QLatin1String("backupOfExistingFile"), v);
    return xml;
}

////////////////////////////////////////////////////////////////////////////
// KDUpdater::MkdirOperation
////////////////////////////////////////////////////////////////////////////

MkdirOperation::MkdirOperation()
{
    setName(QLatin1String("Mkdir"));
}

void MkdirOperation::backup()
{
    QString path = arguments().first();
    path.replace(QLatin1Char('\\'), QLatin1Char('/'));

    QDir createdDir = QDir::root();

    // find out, which part of the path is the first one we actually need to create
    int end = 0;
    while (true) {
        QString p = path.section(QLatin1Char('/'), 0, ++end);
        createdDir = QDir(p);
        if (!createdDir.exists())
            break;
        if (p == path) {
            // everything did already exist -> nothing to do for us (nothing to revert then, either)
            createdDir = QDir::root();
            break;
        }
    }

    setValue(QLatin1String("createddir"), createdDir.absolutePath());
}

bool MkdirOperation::performOperation()
{
    // Requires only one parameter. That is the path which should be created
    QStringList args = this->arguments();
    if (args.count() != 1) {
        setError(InvalidArguments);
        setErrorString(tr("Invalid arguments: %1 arguments given, 1 expected.").arg(args.count()));
        return false;
    }

    const QString dirName = args.first();
    const bool created = QDir::root().mkpath(dirName);
    if (!created) {
        setError(UserDefinedError);
        setErrorString(tr("Could not create folder %1: Unknown error.").arg(dirName));
    }
    return created;
}

bool MkdirOperation::undoOperation()
{
    Q_ASSERT(arguments().count() == 1);

    QString createdDirValue = value(QLatin1String("createddir")).toString();
    if (createdDirValue.isEmpty())
        createdDirValue = arguments().first();
    QDir createdDir = QDir(createdDirValue);
    const bool forceremoval = QVariant(value(QLatin1String("forceremoval"))).toBool();

    // Since refactoring we know the mkdir operation which is creating the target path. If we do a full
    // uninstall prevent removing the full path including target, instead remove the target only. (QTIFW-46)
    if (hasValue(QLatin1String("uninstall-only")) && value(QLatin1String("uninstall-only")).toBool())
        createdDir = QDir(arguments().first());

    if (createdDir == QDir::root())
        return true;

    if (!createdDir.exists())
        return true;

    QString errorString;

    const bool result = removeDirectory(createdDir.path(), &errorString, forceremoval);

    if (!result) {
        if (errorString.isEmpty())
            setError(UserDefinedError, tr("Cannot remove directory %1: %2").arg(createdDir.path(), errorString));
        else
            setError(UserDefinedError, tr("Cannot remove directory %1: %2").arg(createdDir.path(), errnoToQString(errno)));
    }
    return result;
}

bool KDUpdater::MkdirOperation::testOperation()
{
    // TODO
    return true;
}

MkdirOperation *MkdirOperation::clone() const
{
    return new MkdirOperation;
}

////////////////////////////////////////////////////////////////////////////
// KDUpdater::RmdirOperation
////////////////////////////////////////////////////////////////////////////

RmdirOperation::RmdirOperation()
{
    setName(QLatin1String("Rmdir"));
    setValue(QLatin1String("removed"), false);
}

void RmdirOperation::backup()
{
    // nothing to backup - rollback will just create the directory
}

bool RmdirOperation::performOperation()
{
    // Requires only one parameter. That is the name of the file to remove.
    const QStringList args = this->arguments();
    if (args.count() != 1) {
        setError(InvalidArguments);
        setErrorString(tr("Invalid arguments: %1 arguments given, 1 expected.").arg(args.count()));
        return false;
    }

    QDir dir(args.first());
    if (!dir.exists()) {
        setError(UserDefinedError);
        setErrorString(tr("Could not remove folder %1: The folder does not exist.").arg(args.first()));
        return false;
    }

    errno = 0;
    const bool removed = dir.rmdir(args.first());
    setValue(QLatin1String("removed"), removed);
    if (!removed) {
        setError(UserDefinedError);
        setErrorString(tr("Could not remove folder %1: %2").arg(args.first(), errnoToQString(errno)));
    }
    return removed;
}

bool RmdirOperation::undoOperation()
{
   if (!value(QLatin1String("removed")).toBool())
        return true;

    errno = 0;
    const QFileInfo fi(arguments().first());
    const bool success = fi.dir().mkdir(fi.fileName());
    if( !success)
        setError(UserDefinedError, tr("Cannot recreate directory %1: %2").arg(fi.fileName(), errnoToQString(errno)));

    return success;
}

bool RmdirOperation::testOperation()
{
    // TODO
    return true;
}

RmdirOperation *RmdirOperation::clone() const
{
    return new RmdirOperation;
}


////////////////////////////////////////////////////////////////////////////
// KDUpdater::AppendFileOperation
////////////////////////////////////////////////////////////////////////////

AppendFileOperation::AppendFileOperation()
{
    setName(QLatin1String("AppendFile"));
}

void AppendFileOperation::backup()
{
    const QString filename = arguments().first();

    QFile file(filename);
    if (!file.exists())
        return; // nothing to backup

    setValue(QLatin1String("backupOfFile"), backupFileName(filename));
    if (!file.copy(value(QLatin1String("backupOfFile")).toString())) {
        setError(UserDefinedError, tr("Cannot backup file %1: %2").arg(filename, file.errorString()));
        clearValue(QLatin1String("backupOfFile"));
    }
}

bool AppendFileOperation::performOperation()
{
    // This operation takes two arguments. First argument is the name of the file into which a text has to be
    // appended. Second argument is the text to append.
    const QStringList args = this->arguments();
    if (args.count() != 2) {
        setError(InvalidArguments);
        setErrorString(tr("Invalid arguments in %0: %1 arguments given, %2 expected%3.")
            .arg(name()).arg(arguments().count()).arg(tr("exactly 2"), QLatin1String("")));
        return false;
    }

    const QString fName = args.first();
    QFile file(fName);
    if (!file.open(QFile::Append)) {
        // first we rename the file, then we copy it to the real target and open the copy - the renamed original is then marked for deletion
        bool error = false;
        const QString newName = backupFileName(fName);

        if (!QFile::rename(fName, newName))
            error = true;

        if (!error && !QFile::copy(newName, fName)) {
            error = true;
            QFile::rename(newName, fName);
        }

        if (!error && !file.open(QFile::Append)) {
            error = true;
            deleteFileNowOrLater(newName);
        }

        if (error) {
            setError(UserDefinedError);
            setErrorString(tr("Could not open file '%1' for writing: %2").arg(file.fileName(), file.errorString()));
            return false;
        }
        deleteFileNowOrLater(newName);
    }

    QTextStream ts(&file);
    ts << args.last();
    file.close();

    return true;
}

bool AppendFileOperation::undoOperation()
{
   // backupOfFile being empty -> file didn't exist before -> no error
    const QString filename = arguments().first();
    const QString backupOfFile = value(QLatin1String("backupOfFile")).toString();
    if (!backupOfFile.isEmpty() && !QFile::exists(backupOfFile)) {
        setError(UserDefinedError, tr("Cannot find backup file for %1.").arg(filename));
        return false;
    }

    const bool removed = deleteFileNowOrLater(filename);
    if (!removed) {
        setError(UserDefinedError, tr("Could not restore backup file for %1.").arg(filename));
        return false;
    }

    // got deleted? We might be done, if it didn't exist before
    if (backupOfFile.isEmpty())
        return true;

    QFile backupFile(backupOfFile);
    const bool success = backupFile.rename(filename);
    if (!success)
        setError(UserDefinedError, tr("Could not restore backup file for %1: %2").arg(filename, backupFile.errorString()));
    return success;
}

bool AppendFileOperation::testOperation()
{
    // TODO
    return true;
}

AppendFileOperation *AppendFileOperation::clone() const
{
    return new AppendFileOperation;
}


////////////////////////////////////////////////////////////////////////////
// KDUpdater::PrependFileOperation
////////////////////////////////////////////////////////////////////////////

PrependFileOperation::PrependFileOperation()
{
    setName(QLatin1String("PrependFile"));
}

void PrependFileOperation::backup()
{
    const QString filename = arguments().first();

    QFile file(filename);
    if (!file.exists())
        return; // nothing to backup

    setValue(QLatin1String("backupOfFile"), backupFileName(filename));
    if (!file.copy(value(QLatin1String("backupOfFile")).toString())) {
        setError(UserDefinedError, tr("Cannot backup file %1: %2").arg(filename, file.errorString()));
        clearValue(QLatin1String("backupOfFile"));
    }
}

bool PrependFileOperation::performOperation()
{
    // This operation takes two arguments. First argument is the name
    // of the file into which a text has to be appended. Second argument
    // is the text to append.
    const QStringList args = this->arguments();
    if (args.count() != 2) {
        setError(InvalidArguments);
        setErrorString(tr("Invalid arguments: %1 arguments given, 2 expected.").arg(args.count()));
        return false;
    }

    const QString fName = args.first();
    // Load the file first.
    QFile file(fName);
    if (!file.open(QFile::ReadOnly)) {
        setError(UserDefinedError);
        setErrorString(tr("Could not open file %1 for reading: %2").arg(file.fileName(), file.errorString()));
        return false;
    }

    // TODO: fix this, use a text stream
    QString fContents(QLatin1String(file.readAll()));
    file.close();

    // Prepend text to the file text
    fContents = args.last() + fContents;

    // Now re-open the file in write only mode.
    if (!file.open(QFile::WriteOnly)) {
        // first we rename the file, then we copy it to the real target and open the copy - the renamed original is then marked for deletion
        const QString newName = backupFileName(fName);
        if (!QFile::rename(fName, newName) && QFile::copy(newName, fName) && file.open(QFile::WriteOnly)) {
            QFile::rename(newName, fName);
            setError(UserDefinedError);
            setErrorString(tr("Could not open file %1 for writing: %2").arg(file.fileName(), file.errorString()));
            return false;
        }
        deleteFileNowOrLater(newName);
    }
    QTextStream ts(&file);
    ts << fContents;
    file.close();

    return true;
}

bool PrependFileOperation::undoOperation()
{
    // bockupOfFile being empty -> file didn't exist before -> no error
    const QString filename = arguments().first();
    const QString backupOfFile = value(QLatin1String("backupOfFile")).toString();
    if (!backupOfFile.isEmpty() && !QFile::exists(backupOfFile)) {
        setError(UserDefinedError, tr("Cannot find backup file for %1.").arg(filename));
        return false;
    }

    if (!deleteFileNowOrLater(filename)) {
        setError(UserDefinedError, tr("Cannot restore backup file for %1.").arg(filename));
        return false;
    }

    // got deleted? We might be done, if it didn't exist before
    if (backupOfFile.isEmpty())
        return true;

    QFile backupF(backupOfFile);
    const bool success = backupF.rename(filename);
    if (!success)
        setError(UserDefinedError, tr("Cannot restore backup file for %1: %2").arg(filename, backupF.errorString()));

    return success;
}

bool PrependFileOperation::testOperation()
{
    // TODO
    return true;
}

PrependFileOperation *PrependFileOperation::clone() const
{
    return new PrependFileOperation;
}
