/****************************************************************************
**
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing
**
** This file is part of the Qt Build Suite.
**
** 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.
**
** In addition, 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.
**
****************************************************************************/

#include "fileinfo.h"

#include <logging/translator.h>
#include <tools/hostosinfo.h>
#include <tools/qbsassert.h>

#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFileInfo>

#if defined(Q_OS_UNIX)
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#elif defined(Q_OS_WIN)
#include <qt_windows.h>
#endif

namespace qbs {
namespace Internal {

QString FileInfo::fileName(const QString &fp)
{
    int last = fp.lastIndexOf(QLatin1Char('/'));
    if (last < 0)
        return fp;
    return fp.mid(last + 1);
}

QString FileInfo::baseName(const QString &fp)
{
    QString fn = fileName(fp);
    int dot = fn.indexOf(QLatin1Char('.'));
    if (dot < 0)
        return fp;
    return fn.mid(0, dot);
}

QString FileInfo::completeBaseName(const QString &fp)
{
    QString fn = fileName(fp);
    int dot = fn.lastIndexOf(QLatin1Char('.'));
    if (dot < 0)
        return fp;
    return fn.mid(0, dot);
}

QString FileInfo::path(const QString &fp)
{
    if (fp.isEmpty())
        return QString();
    if (fp.at(fp.size() - 1) == QLatin1Char('/'))
        return fp;
    int last = fp.lastIndexOf(QLatin1Char('/'));
    if (last < 0)
        return QLatin1String(".");
    QString p = QDir::cleanPath(fp.mid(0, last));
    if (p.isEmpty() || (HostOsInfo::isWindowsHost() && p.length() == 2 && p.at(0).isLetter()
            && p.at(1) == QLatin1Char(':'))) {
        // Make sure we don't return Windows drive roots without an ending slash.
        // Those paths are considered relative.
        p.append(QLatin1Char('/'));
    }
    return p;
}

void FileInfo::splitIntoDirectoryAndFileName(const QString &filePath, QString *dirPath, QString *fileName)
{
    int idx = filePath.lastIndexOf(QLatin1Char('/'));
    if (idx < 0) {
        dirPath->clear();
        *fileName = filePath;
        return;
    }
    *dirPath = filePath.left(idx);
    *fileName = filePath.mid(idx + 1);
}

void FileInfo::splitIntoDirectoryAndFileName(const QString &filePath, QStringRef *dirPath, QStringRef *fileName)
{
    int idx = filePath.lastIndexOf(QLatin1Char('/'));
    if (idx < 0) {
        dirPath->clear();
        *fileName = QStringRef(&filePath);
        return;
    }
    *dirPath = filePath.leftRef(idx);
    *fileName = filePath.midRef(idx + 1);
}

bool FileInfo::exists(const QString &fp)
{
    return FileInfo(fp).exists();
}

// from creator/src/shared/proparser/ioutils.cpp
bool FileInfo::isAbsolute(const QString &path)
{
    const int n = path.size();
    if (n == 0)
        return false;
    const QChar at0 = path.at(0);
    if (at0 == QLatin1Char('/'))
        return true;
    if (HostOsInfo::isWindowsHost()) {
        if (at0 == QLatin1Char('\\'))
            return true;
        // Unlike QFileInfo, this won't accept a relative path with a drive letter.
        // Such paths result in a royal mess anyway ...
        if (n >= 3 && path.at(1) == QLatin1Char(':') && at0.isLetter()
                && (path.at(2) == QLatin1Char('/') || path.at(2) == QLatin1Char('\\')))
            return true;
    }
    return false;
}

bool FileInfo::isPattern(const QString &str)
{
    return isPattern(QStringRef(&str));
}

bool FileInfo::isPattern(const QStringRef &str)
{
    for (int i = 0; i < str.size(); ++i) {
        const QChar ch = str.at(i);
        if (ch == QLatin1Char('*') || ch == QLatin1Char('?')
                || ch == QLatin1Char(']') || ch == QLatin1Char('[')) {
            return true;
        }
    }
    return false;
}

/**
 * Concatenates the paths \a base and \a rel.
 * Base must be an absolute path.
 * Double dots at the start of \a rel are handled.
 * This function assumes that both paths are clean, that is they don't contain
 * double slashes or redundant dot parts.
 */
QString FileInfo::resolvePath(const QString &base, const QString &rel)
{
    QBS_ASSERT(isAbsolute(base), qDebug("base: %s, rel: %s", qPrintable(base), qPrintable(rel));
            return QString());
    if (isAbsolute(rel))
        return rel;
    if (rel.size() == 1 && rel.at(0) == QLatin1Char('.'))
        return base;
    if (rel.size() == 1 && rel.at(0) == QLatin1Char('~'))
        return QDir::homePath();
    if (rel.startsWith(QLatin1String("~/")))
        return QDir::homePath() + rel.mid(1);

    QString r = base;
    if (r.endsWith(QLatin1Char('/')))
        r.chop(1);

    QString s = rel;
    while (s.startsWith(QLatin1String("../"))) {
        s.remove(0, 3);
        int idx = r.lastIndexOf(QLatin1Char('/'));
        if (idx >= 0)
            r.truncate(idx);
    }
    if (s == QLatin1String("..")) {
        int idx = r.lastIndexOf(QLatin1Char('/'));
        if (idx >= 0)
            r.truncate(idx);
        return r;
    }

    r.reserve(r.length() + 1 + s.length());
    r += QLatin1Char('/');
    r += s;
    return r;
}

bool FileInfo::globMatches(const QRegExp &regexp, const QString &fileName)
{
    const QString pattern = regexp.pattern();
    // May be it's simple wildcard, i.e. "*.cpp"?
    if (pattern.startsWith(QLatin1Char('*')) && !isPattern(pattern.midRef(1))) {
        // Yes, it's rather simple to just check the extension
        return fileName.endsWith(pattern.midRef(1));
    }
    return regexp.exactMatch(fileName);
}

bool FileInfo::isFileCaseCorrect(const QString &filePath)
{
#if defined(Q_OS_WIN)
    // QFileInfo::canonicalFilePath() does not return the real case of the file path on Windows.
    QFileInfo fi(filePath);
    const QString absolute = fi.absoluteFilePath();
    WIN32_FIND_DATA fd;
    HANDLE hFindFile = ::FindFirstFile((wchar_t*)absolute.utf16(), &fd);
    if (hFindFile == INVALID_HANDLE_VALUE)
        return false;
    const QString actualFileName = QString::fromWCharArray(fd.cFileName);
    FindClose(hFindFile);
    return actualFileName == fi.fileName();
#elif defined(Q_OS_DARWIN)
    QFileInfo fi(filePath);
    return fi.fileName() == fileName(fi.canonicalFilePath());
#else
    Q_UNUSED(filePath)
    return true;
#endif
}

bool FileInfo::fileExists(const QFileInfo &fi)
{
    return fi.isSymLink() || fi.exists();
}

#if defined(Q_OS_WIN)

#define z(x) reinterpret_cast<WIN32_FILE_ATTRIBUTE_DATA*>(const_cast<FileInfo::InternalStatType*>(&x))

template<bool> struct CompileTimeAssert;
template<> struct CompileTimeAssert<true> {};

FileInfo::FileInfo(const QString &fileName)
{
    static CompileTimeAssert<
        sizeof(FileInfo::InternalStatType) == sizeof(WIN32_FILE_ATTRIBUTE_DATA)
            > internal_type_has_wrong_size;
    Q_UNUSED(internal_type_has_wrong_size);
    if (!GetFileAttributesEx(reinterpret_cast<const WCHAR*>(fileName.utf16()),
                             GetFileExInfoStandard, &m_stat))
    {
        ZeroMemory(z(m_stat), sizeof(WIN32_FILE_ATTRIBUTE_DATA));
        z(m_stat)->dwFileAttributes = INVALID_FILE_ATTRIBUTES;
    }
}

bool FileInfo::exists() const
{
    return z(m_stat)->dwFileAttributes != INVALID_FILE_ATTRIBUTES;
}

FileTime FileInfo::lastModified() const
{
    const FileTime::InternalType* ft_it;
    ft_it = reinterpret_cast<const FileTime::InternalType*>(&z(m_stat)->ftLastWriteTime);
    return FileTime(*ft_it);
}

FileTime FileInfo::lastStatusChange() const
{
    return lastModified();
}

bool FileInfo::isDir() const
{
    return z(m_stat)->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY;
}

static QString resolveSymlinks(const QString &fileName)
{
    QFileInfo fi(fileName);
    while (fi.isSymLink())
        fi.setFile(fi.symLinkTarget());
    return fi.absoluteFilePath();
}

QString applicationDirPath()
{
    static const QString appDirPath = FileInfo::path(resolveSymlinks(QCoreApplication::applicationFilePath()));
    return appDirPath;
}

#elif defined(Q_OS_UNIX)

FileInfo::FileInfo(const QString &fileName)
{
    if (stat(fileName.toLocal8Bit(), &m_stat) == -1)
        m_stat.st_mtime = 0;
}

bool FileInfo::exists() const
{
    return m_stat.st_mtime != 0;
}

FileTime FileInfo::lastModified() const
{
    return m_stat.st_mtime;
}

FileTime FileInfo::lastStatusChange() const
{
    return m_stat.st_ctime;
}

bool FileInfo::isDir() const
{
    return S_ISDIR(m_stat.st_mode);
}

#endif

// adapted from qtc/plugins/vcsbase/cleandialog.cpp
bool removeFileRecursion(const QFileInfo &f, QString *errorMessage)
{
    if (!FileInfo::fileExists(f))
        return true;
    if (f.isDir() && !f.isSymLink()) {
        const QDir dir(f.absoluteFilePath());

        // QDir::System is needed for broken symlinks.
        foreach (const QFileInfo &fi, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot
                                                       | QDir::Hidden | QDir::System))
            removeFileRecursion(fi, errorMessage);
        QDir parent = f.absoluteDir();
        if (!parent.rmdir(f.fileName())) {
            errorMessage->append(Tr::tr("The directory %1 could not be deleted.").
                                 arg(QDir::toNativeSeparators(f.absoluteFilePath())));
            return false;
        }
    } else {
        QFile file(f.absoluteFilePath());
        file.setPermissions(f.permissions() | QFile::WriteUser);
        if (!file.remove()) {
            if (!errorMessage->isEmpty())
                errorMessage->append(QLatin1Char('\n'));
            errorMessage->append(Tr::tr("The file %1 could not be deleted.").
                                 arg(QDir::toNativeSeparators(f.absoluteFilePath())));
            return false;
        }
    }
    return true;
}

bool removeDirectoryWithContents(const QString &path, QString *errorMessage)
{
    QFileInfo f(path);
    if (f.exists() && !f.isDir()) {
        *errorMessage = Tr::tr("%1 is not a directory.").arg(QDir::toNativeSeparators(path));
        return false;
    }
    return removeFileRecursion(f, errorMessage);
}

/*!
 * Returns the stored link target of the symbolic link \a{filePath}.
 * Unlike QFileInfo::symLinkTarget, this will not make the link target an absolute path.
 */
static QByteArray storedLinkTarget(const QString &filePath)
{
    QByteArray result;

#ifdef Q_OS_UNIX
    const QByteArray nativeFilePath = QFile::encodeName(filePath);
    ssize_t len;
    while (true) {
        struct stat sb;
        if (lstat(nativeFilePath.constData(), &sb)) {
            qWarning("storedLinkTarget: lstat for %s failed with error code %d",
                     nativeFilePath.constData(), errno);
            return QByteArray();
        }

        result.resize(sb.st_size);
        len = readlink(nativeFilePath.constData(), result.data(), sb.st_size + 1);
        if (len < 0) {
            qWarning("storedLinkTarget: readlink for %s failed with error code %d",
                     nativeFilePath.constData(), errno);
            return QByteArray();
        }

        if (len < sb.st_size) {
            result.resize(len);
            break;
        }
        if (len == sb.st_size)
            break;
    }
#else
    Q_UNUSED(filePath);
#endif // Q_OS_UNIX

    return result;
}

static bool createSymLink(const QByteArray &path1, const QString &path2)
{
#ifdef Q_OS_UNIX
    const QByteArray newPath = QFile::encodeName(path2);
    unlink(newPath.constData());
    return symlink(path1.constData(), newPath.constData()) == 0;
#else
    Q_UNUSED(path1);
    Q_UNUSED(path2);
    return false;
#endif // Q_OS_UNIX
}

/*!
  Copies the directory specified by \a srcFilePath recursively to \a tgtFilePath.
  \a tgtFilePath will contain the target directory, which will be created. Example usage:

  \code
    QString error;
    book ok = Utils::FileUtils::copyRecursively("/foo/bar", "/foo/baz", &error);
    if (!ok)
      qDebug() << error;
  \endcode

  This will copy the contents of /foo/bar into to the baz directory under /foo,
  which will be created in the process.

  \return Whether the operation succeeded.
  \note Function was adapted from qtc/src/libs/fileutils.cpp
*/

bool copyFileRecursion(const QString &srcFilePath, const QString &tgtFilePath,
        bool preserveSymLinks, QString *errorMessage)
{
    QFileInfo srcFileInfo(srcFilePath);
    QFileInfo tgtFileInfo(tgtFilePath);
    const QString targetDirPath = tgtFileInfo.absoluteDir().path();
    if (!QDir::root().mkpath(targetDirPath)) {
        *errorMessage = Tr::tr("The directory '%1' could not be created.")
                .arg(QDir::toNativeSeparators(targetDirPath));
        return false;
    }
    if (HostOsInfo::isAnyUnixHost() && preserveSymLinks && srcFileInfo.isSymLink()) {
        // For now, disable symlink preserving copying on Windows.
        // MS did a good job to prevent people from using symlinks - even if they are supported.
        if (!createSymLink(storedLinkTarget(srcFilePath), tgtFilePath)) {
            *errorMessage = Tr::tr("The symlink '%1' could not be created.")
                    .arg(tgtFilePath);
            return false;
        }
    } else if (srcFileInfo.isDir()) {
        QDir sourceDir(srcFilePath);
        QStringList fileNames = sourceDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot
            | QDir::Hidden | QDir::System);
        foreach (const QString &fileName, fileNames) {
            const QString newSrcFilePath = srcFilePath + QLatin1Char('/') + fileName;
            const QString newTgtFilePath = tgtFilePath + QLatin1Char('/') + fileName;
            if (!copyFileRecursion(newSrcFilePath, newTgtFilePath, preserveSymLinks, errorMessage))
                return false;
        }
    } else {
        if (tgtFileInfo.exists() && srcFileInfo.lastModified() <= tgtFileInfo.lastModified())
            return true;
        QFile file(srcFilePath);
        QFile targetFile(tgtFilePath);
        if (targetFile.exists()) {
            targetFile.setPermissions(targetFile.permissions() | QFile::WriteUser);
            if (!targetFile.remove()) {
                *errorMessage = Tr::tr("Could not remove file '%1'. %2")
                        .arg(QDir::toNativeSeparators(tgtFilePath), targetFile.errorString());
            }
        }
        if (!file.copy(tgtFilePath)) {
            *errorMessage = Tr::tr("Could not copy file '%1' to '%2'. %3")
                .arg(QDir::toNativeSeparators(srcFilePath), QDir::toNativeSeparators(tgtFilePath),
                     file.errorString());
            return false;
        }
    }
    return true;
}

} // namespace Internal
} // namespace qbs
