// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "fileutils.h"
#include "savefile.h"

#include "algorithm.h"
#include "environment.h"
#include "filedialog.h"
#include "qtcassert.h"
#include "utilstr.h"

#include "fsengine/fileiconprovider.h"

#include <QDataStream>
#include <QDateTime>
#include <QDebug>
#include <QFileInfo>
#include <QRegularExpression>
#include <QTemporaryFile>
#include <QTextStream>
#include <QXmlStreamWriter>


#ifdef QT_GUI_LIB
#include "guiutils.h"

#include <QMessageBox>
#include <QGuiApplication>
#endif

#ifdef Q_OS_WIN
#ifdef QTCREATOR_PCH_H
#define CALLBACK WINAPI
#endif
#include <qt_windows.h>
#include <shlobj.h>
#include <winioctl.h>
#endif

#ifdef Q_OS_MACOS
#include "fileutils_mac.h"
#endif

namespace Utils {

// FileSaver

FileSaverBase::FileSaverBase()
    : m_result(ResultOk)
{}

FileSaverBase::~FileSaverBase() = default;

Result<> FileSaverBase::finalize()
{
    if (!m_file) {
        // The saver was never opened - e.g. a remote temporary file could not be created.
        // m_result already carries the reason; return it instead of asserting.
        return m_result ? ResultError(Tr::tr("File was not opened for writing.")) : m_result;
    }
    m_file->close();
    setResult(m_file->error() == QFile::NoError);
    m_file.reset();
    return m_result;
}

bool FileSaverBase::write(const QByteArrayView bytes)
{
    if (!m_result)
        return false;
    return setResult(m_file->write(bytes.data(), bytes.size()) == bytes.size());
}

bool FileSaverBase::setResult(bool ok)
{
    if (!ok && m_result) {
        if (!m_file->errorString().isEmpty()) {
            m_result = ResultError(Tr::tr("Cannot write file %1: %2")
                                     .arg(m_filePath.toUserOutput(), m_file->errorString()));
        } else {
            m_result = ResultError(Tr::tr("Cannot write file %1. Disk full?")
                                     .arg(m_filePath.toUserOutput()));
        }
    }
    return ok;
}

bool FileSaverBase::setResult(QTextStream *stream)
{
    stream->flush();
    return setResult(stream->status() == QTextStream::Ok);
}

bool FileSaverBase::setResult(QDataStream *stream)
{
    return setResult(stream->status() == QDataStream::Ok);
}

bool FileSaverBase::setResult(QXmlStreamWriter *stream)
{
    return setResult(!stream->hasError());
}

// FileSaver
FileSaver::FileSaver(const FilePath &filePath, QIODevice::OpenMode mode)
{
    m_filePath = filePath;
    // Workaround an assert in Qt -- and provide a useful error message, too:
    if (m_filePath.osType() == OsType::OsTypeWindows) {
        // Taken from: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
        static const QStringList reservedNames
                = {"CON", "PRN", "AUX", "NUL",
                   "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
                   "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"};
        const QString fn = filePath.baseName().toUpper();
        if (reservedNames.contains(fn)) {
            m_result = ResultError(Tr::tr("%1: Is a reserved filename on Windows. Cannot save.")
                                     .arg(filePath.toUserOutput()));
            return;
        }
    }

    const bool readOnlyOrAppend = mode & (QIODevice::ReadOnly | QIODevice::Append);
    m_isSafe = !readOnlyOrAppend && !qtcEnvironmentVariableIsSet("QTC_DISABLE_ATOMICSAVE")
               && filePath.supportsAtomicSaveFile();
    if (m_isSafe)
        m_file.reset(new SaveFile(filePath));
    else
        m_file.reset(new QFile{filePath.toFSPathString()});

    if (!m_file->open(QIODevice::WriteOnly | mode)) {
        QString err = filePath.exists() ?
                Tr::tr("Cannot overwrite file %1: %2") : Tr::tr("Cannot create file %1: %2");
        m_result = ResultError(err.arg(filePath.toUserOutput(), m_file->errorString()));
    }
}

Result<> FileSaver::finalize()
{
    if (!m_isSafe)
        return FileSaverBase::finalize();

    auto sf = static_cast<SaveFile *>(m_file.get());
    if (!m_result) {
        if (sf->isOpen())
            sf->rollback();
    } else {
        setResult(sf->commit());
    }
    m_file.reset();
    return m_result;
}

TempFileSaver::TempFileSaver(const QString &templ)
{
    initFromString(templ);
}

void TempFileSaver::initFromString(const QString &templ)
{
    m_file.reset(new QTemporaryFile{});
    auto tempFile = static_cast<QTemporaryFile *>(m_file.get());
    if (!templ.isEmpty())
        tempFile->setFileTemplate(templ);
    tempFile->setAutoRemove(false);
    if (!tempFile->open()) {
        m_result = ResultError(Tr::tr("Cannot create temporary file in %1: %2").arg(
                QDir::toNativeSeparators(QFileInfo(tempFile->fileTemplate()).absolutePath()),
                tempFile->errorString()));
    }
    m_filePath = FilePath::fromString(tempFile->fileName());
}

TempFileSaver::TempFileSaver(const FilePath &templ)
{
    if (templ.isEmpty() || templ.isLocal()) {
        initFromString(templ.path());
    } else {
        Result<FilePath> result = templ.createTempFile();
        if (!result) {
            m_result = ResultError(Tr::tr("Cannot create temporary file %1: %2")
                                .arg(templ.toUserOutput(), result.error()));
            return;
        }

        m_file.reset(new QFile(result->toFSPathString()));
        if (!m_file->open(QIODevice::WriteOnly)) {
            m_result = ResultError(Tr::tr("Cannot create temporary file %1: %2")
                                .arg(result->toUserOutput(), m_file->errorString()));
        }
        m_filePath = *result;
    }
}

TempFileSaver::~TempFileSaver()
{
    m_file.reset();
    if (m_autoRemove && !m_filePath.isEmpty())
        m_filePath.removeFile();
}

/*!
    \namespace Utils::FileUtils
    \inmodule QtCreator

  \brief The FileUtils namespace contains file and directory related convenience
  functions.

*/
namespace FileUtils {

#ifdef QT_GUI_LIB
CopyAskingForOverwrite::CopyAskingForOverwrite(const std::function<bool (FilePath)> &postOperation)
    : m_postOperation(postOperation)
{}

CopyHelper CopyAskingForOverwrite::operator()()
{
    CopyHelper helperFunction =
            [this](const FilePath &src, const FilePath &dest) -> Result<CopyResult> {
        bool copyFile = true;
        if (dest.exists()) {
            if (m_skipAll)
                copyFile = false;
            else if (!m_overwriteAll) {
                const int res = QMessageBox::question(
                    dialogParent(),
                    Tr::tr("Overwrite File?"),
                    Tr::tr("Overwrite existing file \"%1\"?").arg(dest.toUserOutput()),
                    QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No
                            | QMessageBox::NoToAll | QMessageBox::Cancel);

                if (res == QMessageBox::Cancel)
                    return CopyResult::Canceled;

                if (res == QMessageBox::No) {
                    copyFile = false;
                } else if (res == QMessageBox::NoToAll) {
                    m_skipAll = true;
                    copyFile = false;
                } else if (res == QMessageBox::YesToAll) {
                    m_overwriteAll = true;
                }
                if (copyFile)
                    dest.removeFile();
            } else {
                dest.removeFile();
            }
        }
        if (copyFile) {
            dest.parentDir().ensureWritableDir();
            if (!src.copyFile(dest)) {
                return ResultError(Tr::tr("Could not copy file \"%1\" to \"%2\".")
                                       .arg(src.toUserOutput(), dest.toUserOutput()));
            }
            if (m_postOperation) {
                if (!m_postOperation(dest))
                    return CopyResult::Canceled;
            }
        }
        m_files.append(dest.absoluteFilePath());
        return CopyResult::Done;
    };
    return helperFunction;
}

FilePaths CopyAskingForOverwrite::files() const
{
    return m_files;
}
#endif // QT_GUI_LIB

FilePath commonPath(const FilePaths &paths)
{
    return paths.commonPath();
}

#ifdef QT_WIDGETS_LIB

QString fetchQrc(const QString &fileName)
{
    QTC_ASSERT(fileName.startsWith(':'), return QString());
    QFile file(fileName);
    bool ok = file.open(QIODevice::ReadOnly);
    QTC_ASSERT(ok, qWarning() << fileName << "not there!"; return QString());
    return QString::fromUtf8(file.readAll());
}

static FilePaths getFilePaths(
    const QString &caption,
    const FilePath &dir,
    const QString &filter,
    QString *selectedFilter,
    QFileDialog::Options options,
    const QStringList &supportedSchemes,
    const bool forceNonNativeDialog,
    QFileDialog::FileMode fileMode,
    QFileDialog::AcceptMode acceptMode)
{
#ifdef QT_DEBUG
    const bool shiftPressed = QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier);
#else
    const bool shiftPressed = false;
#endif
    if (!hasNativeFileDialog() || forceNonNativeDialog || shiftPressed || !dir.isLocal()
        || options.testFlag(QFileDialog::DontUseNativeDialog)) {
        FileDialog dialog(dialogParent());
        dialog.setMode(fileMode);
        if (!caption.isEmpty())
            dialog.setWindowTitle(caption);
        dialog.setDirectory(dir.isEmpty() ? FilePath::fromString(QDir::homePath()) : dir);
        if (!filter.isEmpty())
            dialog.setNameFilters({filter});
        dialog.setAcceptMode(acceptMode);
        if (dialog.exec() == QDialog::Accepted) {
            if (selectedFilter)
                *selectedFilter = dialog.selectedNameFilter();
            return dialog.selectedFiles();
        }
        return {};
    }

    QFileDialog dialog(dialogParent(), caption, dir.toFSPathString(), filter);
    dialog.setFileMode(fileMode);
    dialog.setOptions(options);

    dialog.setSupportedSchemes(supportedSchemes);
    dialog.setAcceptMode(acceptMode);

    if (selectedFilter && !selectedFilter->isEmpty())
        dialog.selectNameFilter(*selectedFilter);
    if (dialog.exec() == QDialog::Accepted) {
        if (selectedFilter)
            *selectedFilter = dialog.selectedNameFilter();
        return Utils::transform(dialog.selectedUrls(), &FilePath::fromUrl);
    }
    return {};
}

static FilePath firstOrEmpty(const FilePaths &filePaths)
{
    return filePaths.isEmpty() ? FilePath() : filePaths.first();
}

bool hasNativeFileDialog()
{
    // Checking QFileDialog::itemDelegate() seems to be the only way to determine
    // whether the dialog is native or not.
    static bool hasNative = QFileDialog().itemDelegate() == nullptr;
    return hasNative;
}

FilePath getOpenFilePath(const QString &caption,
                         const FilePath &dir,
                         const QString &filter,
                         QString *selectedFilter,
                         QFileDialog::Options options,
                         bool fromDeviceIfShiftIsPressed,
                         bool forceNonNativeDialog)
{
    forceNonNativeDialog = forceNonNativeDialog || !dir.isLocal();
#ifdef QT_GUI_LIB
    if (fromDeviceIfShiftIsPressed && qApp->queryKeyboardModifiers() & Qt::ShiftModifier) {
        forceNonNativeDialog = true;
    }
#endif

    const QStringList schemes = QStringList(QStringLiteral("file"));
    return firstOrEmpty(getFilePaths(caption,
                                     dir,
                                     filter,
                                     selectedFilter,
                                     options,
                                     schemes,
                                     forceNonNativeDialog,
                                     QFileDialog::ExistingFile,
                                     QFileDialog::AcceptOpen));
}

FilePath getSaveFilePath(const QString &caption,
                         const FilePath &dir,
                         const QString &filter,
                         QString *selectedFilter,
                         QFileDialog::Options options,
                         bool forceNonNativeDialog)
{
    forceNonNativeDialog = forceNonNativeDialog || !dir.isLocal();

    const QStringList schemes = QStringList(QStringLiteral("file"));
    return firstOrEmpty(getFilePaths(caption,
                                     dir,
                                     filter,
                                     selectedFilter,
                                     options,
                                     schemes,
                                     forceNonNativeDialog,
                                     QFileDialog::AnyFile,
                                     QFileDialog::AcceptSave));
}

FilePath getExistingDirectory(const QString &caption,
                              const FilePath &dir,
                              QFileDialog::Options options,
                              bool fromDeviceIfShiftIsPressed,
                              bool forceNonNativeDialog)
{
    forceNonNativeDialog = forceNonNativeDialog || !dir.isLocal();

#ifdef QT_GUI_LIB
    if (fromDeviceIfShiftIsPressed && qApp->queryKeyboardModifiers() & Qt::ShiftModifier) {
        forceNonNativeDialog = true;
    }
#endif

    const QStringList schemes = QStringList(QStringLiteral("file"));
    return firstOrEmpty(getFilePaths(caption,
                                     dir,
                                     {},
                                     nullptr,
                                     options,
                                     schemes,
                                     forceNonNativeDialog,
                                     QFileDialog::Directory,
                                     QFileDialog::AcceptOpen));
}

FilePaths getOpenFilePaths(const QString &caption,
                           const FilePath &dir,
                           const QString &filter,
                           QString *selectedFilter,
                           QFileDialog::Options options)
{
    bool forceNonNativeDialog = !dir.isLocal();

    const QStringList schemes = QStringList(QStringLiteral("file"));
    return getFilePaths(caption,
                        dir,
                        filter,
                        selectedFilter,
                        options,
                        schemes,
                        forceNonNativeDialog,
                        QFileDialog::ExistingFiles,
                        QFileDialog::AcceptOpen);
}

#endif // QT_WIDGETS_LIB

static FilePathInfo::FileFlags fileInfoFlagsfromStatMode(const QString &hexString, int modeBase)
{
    // Copied from stat.h
    enum st_mode {
        IFMT = 00170000,
        IFSOCK = 0140000,
        IFLNK = 0120000,
        IFREG = 0100000,
        IFBLK = 0060000,
        IFDIR = 0040000,
        IFCHR = 0020000,
        IFIFO = 0010000,
        ISUID = 0004000,
        ISGID = 0002000,
        ISVTX = 0001000,
        IRWXU = 00700,
        IRUSR = 00400,
        IWUSR = 00200,
        IXUSR = 00100,
        IRWXG = 00070,
        IRGRP = 00040,
        IWGRP = 00020,
        IXGRP = 00010,
        IRWXO = 00007,
        IROTH = 00004,
        IWOTH = 00002,
        IXOTH = 00001,
    };

    bool ok = false;
    uint mode = hexString.toUInt(&ok, modeBase);

    QTC_ASSERT(ok, return {});

    FilePathInfo::FileFlags result;

    if (mode & IRUSR) {
        result |= FilePathInfo::ReadOwnerPerm;
        result |= FilePathInfo::ReadUserPerm;
    }
    if (mode & IWUSR) {
        result |= FilePathInfo::WriteOwnerPerm;
        result |= FilePathInfo::WriteUserPerm;
    }
    if (mode & IXUSR) {
        result |= FilePathInfo::ExeOwnerPerm;
        result |= FilePathInfo::ExeUserPerm;
    }
    if (mode & IRGRP)
        result |= FilePathInfo::ReadGroupPerm;
    if (mode & IWGRP)
        result |= FilePathInfo::WriteGroupPerm;
    if (mode & IXGRP)
        result |= FilePathInfo::ExeGroupPerm;
    if (mode & IROTH)
        result |= FilePathInfo::ReadOtherPerm;
    if (mode & IWOTH)
        result |= FilePathInfo::WriteOtherPerm;
    if (mode & IXOTH)
        result |= FilePathInfo::ExeOtherPerm;

    if ((mode & IFMT) == IFLNK)
        result |= FilePathInfo::LinkType;
    if ((mode & IFMT) == IFREG)
        result |= FilePathInfo::FileType;
    if ((mode & IFMT) == IFDIR)
        result |= FilePathInfo::DirectoryType;
    if ((mode & IFMT) == IFBLK)
        result |= FilePathInfo::LocalDiskFlag;

    if (result != 0) // There is no Exist flag, but if anything was set before, it must exist.
        result |= FilePathInfo::ExistsFlag;

    return result;
}

FilePathInfo filePathInfoFromTriple(const QString &infos, int modeBase)
{
    const QStringList parts = infos.split(' ', Qt::SkipEmptyParts);
    if (parts.size() != 3)
        return {};

    FilePathInfo::FileFlags flags = fileInfoFlagsfromStatMode(parts[0], modeBase);

    const QDateTime dt = QDateTime::fromSecsSinceEpoch(parts[1].toLongLong(), Qt::UTC);
    qint64 size = parts[2].toLongLong();
    return {size, flags, dt};
}

Result<CopyResult> copyRecursively(const FilePath &srcFilePath,
                                   const FilePath &tgtFilePath,
                                   const CopyHelper &copyHelper)
{
    if (srcFilePath.isDir()) {
        if (Result<> res = tgtFilePath.ensureWritableDir(); !res) {
            return ResultError(Tr::tr("Failed to create directory \"%1\": %2")
                                    .arg(tgtFilePath.toUserOutput(), res.error()));
        }
        const QDir sourceDir(srcFilePath.toFSPathString());
        const QStringList fileNames = sourceDir.entryList(
            QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System);
        for (const QString &fileName : fileNames) {
            const FilePath newSrcFilePath = srcFilePath / fileName;
            const FilePath newTgtFilePath = tgtFilePath / fileName;
            const Result<CopyResult> res =
                copyRecursively(newSrcFilePath, newTgtFilePath, copyHelper);
            if (!res || *res == CopyResult::Canceled)
                return res;
        }
        return CopyResult::Done;
    }

    return copyHelper(srcFilePath, tgtFilePath);
}

/*!
  Copies a file specified by \a srcFilePath to \a tgtFilePath only if \a srcFilePath is different
  (file contents and last modification time).

  Returns whether the operation succeeded.
*/

Result<> copyIfDifferent(const FilePath &srcFilePath, const FilePath &tgtFilePath)
{
    if (!srcFilePath.exists())
        return ResultError(Tr::tr("File %1 does not exist.").arg(srcFilePath.toUserOutput()));

    if (!srcFilePath.isLocal() || !tgtFilePath.isLocal())
        return srcFilePath.copyFile(tgtFilePath);

    if (tgtFilePath.exists()) {
        const QDateTime srcModified = srcFilePath.lastModified();
        const QDateTime tgtModified = tgtFilePath.lastModified();
        if (srcModified == tgtModified) {
            // TODO: Create FilePath::hashFromContents() and compare hashes.
            const Result<QByteArray> srcContents = srcFilePath.fileContents();
            const Result<QByteArray> tgtContents = tgtFilePath.fileContents();
            if (srcContents && srcContents == tgtContents)
                return ResultOk;
        }

        if (Result<> res = tgtFilePath.removeFile(); !res)
            return res;
    }

    return srcFilePath.copyFile(tgtFilePath);
}

QString fileSystemFriendlyName(const QString &name)
{
    QString result = name;
    static const QRegularExpression nonWordEx("\\W");
    result.replace(nonWordEx, QLatin1String("_"));
    static const QRegularExpression subsequentUnderscoreEx("_+");
    result.replace(subsequentUnderscoreEx, QLatin1String("_"));
    static const QRegularExpression leadingUnderscoreEx("^_*");
    result.remove(leadingUnderscoreEx);
    static const QRegularExpression trailingUnderscoreEx("_+$");
    result.remove(trailingUnderscoreEx);
    if (result.isEmpty())
        result = QLatin1String("unknown");
    return result;
}

int indexOfQmakeUnfriendly(const QString &name, int startpos)
{
    static const QRegularExpression checkRegExp(QLatin1String("[^a-zA-Z0-9_.-]"));
    return checkRegExp.match(name, startpos).capturedStart();
}

QString qmakeFriendlyName(const QString &name)
{
    QString result = name;

    // Remove characters that might trip up a build system (especially qmake):
    int pos = indexOfQmakeUnfriendly(result);
    while (pos >= 0) {
        result[pos] = QLatin1Char('_');
        pos = indexOfQmakeUnfriendly(result, pos);
    }
    return fileSystemFriendlyName(result);
}

// makes sure that capitalization of directories is canonical on Windows and macOS.
// This mimics the logic in QDeclarative_isFileCaseCorrect
QString normalizedPathName(const QString &name)
{
#ifdef Q_OS_WIN
    const QString nativeSeparatorName(QDir::toNativeSeparators(name));
    const auto nameC = reinterpret_cast<LPCTSTR>(nativeSeparatorName.utf16()); // MinGW
    PIDLIST_ABSOLUTE file;
    HRESULT hr = SHParseDisplayName(nameC, NULL, &file, 0, NULL);
    if (FAILED(hr))
        return name;
    TCHAR buffer[MAX_PATH];
    const bool success = SHGetPathFromIDList(file, buffer);
    ILFree(file);
    return success ? QDir::fromNativeSeparators(QString::fromUtf16(reinterpret_cast<const char16_t *>(buffer)))
                   : name;
#elif defined(Q_OS_MACOS)
    return Internal::normalizePathName(name);
#else // do not try to handle case-insensitive file systems on Linux
    return name;
#endif
}

FilePath commonPath(const FilePath &oldCommonPath, const FilePath &filePath)
{
    return FilePaths{oldCommonPath, filePath}.commonPath();
}

FilePath homePath()
{
    return FilePath::fromUserInput(QDir::homePath());
}

Result<FilePath> scratchBufferFilePath(const QString &pattern)
{
    QString tmp = pattern;
    QFileInfo fi(tmp);
    if (!fi.isAbsolute()) {
        QString tempPattern = QDir::tempPath();
        if (!tempPattern.endsWith(QLatin1Char('/')))
            tempPattern += QLatin1Char('/');
        tmp = tempPattern + tmp;
    }

    QTemporaryFile file(tmp);
    file.setAutoRemove(false);
    if (!file.open()) {
        return ResultError(Tr::tr("Failed to set up scratch buffer in \"%1\".")
                                   .arg(FilePath::fromString(tmp).parentDir().toUserOutput()));
    }
    file.close();
    return FilePath::fromString(file.fileName());
}

FilePaths toFilePathList(const QStringList &paths)
{
    return FilePaths::fromStrings(paths);
}

qint64 bytesAvailableFromDFOutput(const QByteArray &dfOutput)
{
    const auto lines = filtered(dfOutput.split('\n'),
                                [](const QByteArray &line) { return line.size() > 0; });

    QTC_ASSERT(lines.size() == 2, return -1);
    const auto headers = filtered(lines[0].split(' '),
                                  [](const QByteArray &field) { return field.size() > 0; });
    QTC_ASSERT(headers.size() >= 4, return -1);
    QTC_ASSERT(headers[3] == QByteArray("Available"), return -1);

    const auto fields = filtered(lines[1].split(' '),
                                 [](const QByteArray &field) { return field.size() > 0; });
    QTC_ASSERT(fields.size() >= 4, return -1);

    bool ok = false;
    const quint64 result = QString::fromUtf8(fields[3]).toULongLong(&ok);
    if (ok)
        return result;
    return -1;
}

FilePaths usefulExtraSearchPaths()
{
    if (HostOsInfo::isMacHost()) {
        return {"/opt/homebrew/bin"};
    } else if (HostOsInfo::isWindowsHost()) {
        const QString programData =
            qtcEnvironmentVariable("ProgramData",
                                   qtcEnvironmentVariable("ALLUSERSPROFILE", "C:/ProgramData"));
        return {FilePath::fromUserInput(programData) / "chocolatey/bin"};
    }

    return {};
}

void showError(const QString &errorMessage)
{
    QMessageBox::critical(dialogParent(), Tr::tr("File Error"), errorMessage);
}

} // namespace FileUtils

#ifdef Q_OS_WIN
template <>
void withNtfsPermissions(const std::function<void()> &task)
{
    qt_ntfs_permission_lookup++;
    task();
    qt_ntfs_permission_lookup--;
}
#endif

} // namespace Utils
