/**
 * Copyright (C) 2023-2026 Linaro Limited (or its affiliates). All rights reserved.
 * Copyright (C) 2012-2023 Arm Limited (or its affiliates).
 * All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 */

// +explicit-local-file: see header

#include "pathstring.h"
#include <QVarLengthArray>
#include <QDebug>

namespace {
    inline bool isUncPrefix(const QString& path)
    {
        return path == QLatin1String("//") ||
               path == QLatin1String("\\\\");
    }
    inline bool hasUncPrefix(const QString& path)
    {
        return path.startsWith(QLatin1String("//")) ||
               path.startsWith(QLatin1String("\\\\"));
    }
    inline bool isUnixRoot(const QString& path)
    {
        return path == QLatin1String("/") ||
               path == QLatin1String("\\");
    }
    inline bool hasUnixRoot(const QString& path)
    {
        return path.startsWith(QLatin1String("/")) ||
               path.startsWith(QLatin1String("\\"));
    }
    inline bool isWindowsRoot(const QString& path)
    {
        return (path.length() == 2 &&
                path.at(1) == QLatin1Char(':')) ||
               (path.length() == 3 &&
                path.at(1) == QLatin1Char(':') &&
                (path.at(2) == QLatin1Char('\\') ||
                 path.at(2) == QLatin1Char('/')));
    }
    inline bool hasWindowsRoot(const QString& path)
    {
        return (path.length() >= 2 &&
                path.at(1) == QLatin1Char(':'));
    }
    inline bool endsWithSlash(const QString& path)
    {
        return path.endsWith(QLatin1Char('/')) ||
               path.endsWith(QLatin1Char('\\'));
    }
    inline bool isSlash(const QChar& c)
    {
        return c == QLatin1Char('/') ||
               c == QLatin1Char('\\');
    }
    inline int lastIndexOfSlash(const QString& path)
    {
        int const lastSlash = path.lastIndexOf(QLatin1Char('/'));
        int const lastBackslash = path.lastIndexOf(QLatin1Char('\\'));
        if (lastBackslash > lastSlash)
            return lastBackslash;
        else
            return lastSlash;
    }
    inline QChar separator(const QString& path)
    {
        int const lastSlash = path.lastIndexOf(QLatin1Char('/'));
        int const lastBackslash = path.lastIndexOf(QLatin1Char('\\'));
        if (lastBackslash > lastSlash)
            return QLatin1Char('\\');
        else
            return QLatin1Char('/');
    }
}

//! Return the last component of the file name.
/*!
 * The is the file name stripped of any containing directories.
 */
QString PathString::fileName() const
{
    int const lastSlash = lastIndexOfSlash(mFileName);
    return mFileName.mid(lastSlash + 1);
}

//! Return all but the last component of the file name.
/*!
 * This is the directory containing the file.
 */
QString PathString::path() const
{
    int const lastSlash = lastIndexOfSlash(mFileName);
    if (lastSlash == -1)  // No directory info!
        return QString{};
    if (lastSlash == 1 && isUncPrefix(mFileName.left(2)))
        return mFileName.left(2);
    if (lastSlash == 0)
        return mFileName.at(0);
    return mFileName.left(lastSlash);
}

//! Return the file name resulting from placing *fileName* in this directory.
QString PathString::filePath(const QString &fileName) const
{
    if (hasUnixRoot(fileName) ||
        hasWindowsRoot(fileName) ||
        hasUncPrefix(fileName))
        return fileName;
    if (endsWithSlash(mFileName))
        return mFileName + fileName;
    QChar slash(separator(mFileName));
    return mFileName + slash + fileName;
}

//! Return the file name suffix (file extension in Windows terminology).
QString PathString::suffix() const
{
    QString const fileName(this->fileName());
    int const lastDot = fileName.lastIndexOf(QLatin1Char('.'));
    if (lastDot == -1)
        return QString{};
    return fileName.mid(lastDot + 1);
}

//! Return the whole file name (including directories) minus the suffix.
QString PathString::completeBaseName() const
{
    QString const fileName(this->fileName());
    int const lastDot = fileName.lastIndexOf(QLatin1Char('.'));
    if (lastDot == -1)
        return fileName;
    return fileName.left(lastDot);
}

//! Return `true` if the file name is relative, `false` otherwise.
bool PathString::isRelative() const
{
    return !isAbsolute();
}

//! Return `true` if the file name is absolution, `false` otherwise.
bool PathString::isAbsolute() const
{
    return hasUnixRoot(mFileName) ||
           hasWindowsRoot(mFileName) ||
           hasUncPrefix(mFileName);
}

//! Return `true` if this is the root directory of a file-system.
bool PathString::isRoot() const
{
    return isUnixRoot(mFileName) ||
           isWindowsRoot(mFileName) ||
           isUncPrefix(mFileName);
}

//! Split a full file name into its directories and file components.
QStringList PathString::splitPath() const
{
    QStringList list;   // The list to be returned
    int i = 0;          // Start index of the next substring to add to 'list'

    // Special case the root of the path, if necessary
    if (hasUncPrefix(mFileName))
    {
        list << mFileName.left(2);
        i = 2;
    }
    else if (hasUnixRoot(mFileName))
    {
        list << mFileName.at(0);
        i = 1;
    }

    // Split the remainder of the path. Do this manually rather than using QString::split(QRegExp)
    // as QRegExp is slow. Don't use QString::split(QChar) as we need to cope with both '/' and '\'
    // separators.
    const int fileNameLength = mFileName.length();

    int indexOfUnixSeparator = mFileName.indexOf('/', i);
    if (indexOfUnixSeparator < 0)
        indexOfUnixSeparator = fileNameLength;

    int indexOfWinSeparator  = mFileName.indexOf('\\', i);
    if (indexOfWinSeparator < 0)
        indexOfWinSeparator = fileNameLength;

    int indexOfNextSeparator = -1;  // Will be index of the first separator character ('/' or '\') that is >= i
    do
    {
        // Decide the index of the next separator character, and determine the index of the
        // following separator (of the same type: unix/win) as well.
        //
        // Logically we are after 'index of next separator' i.e.
        //     min ( 'index of next /' , 'index of next \' )
        // but to avoid repeatedly scanning the same set of characters we only recalculate
        // an 'index of next X' value when it will have changed (aka when it is used to
        // extract a substring for 'list').
        if (indexOfUnixSeparator < indexOfWinSeparator)
        {
            indexOfNextSeparator = indexOfUnixSeparator;

            indexOfUnixSeparator = mFileName.indexOf('/', indexOfNextSeparator+1);
            if (indexOfUnixSeparator < 0)
                indexOfUnixSeparator = fileNameLength;
        }
        else
        {
            indexOfNextSeparator = indexOfWinSeparator;

            indexOfWinSeparator  = mFileName.indexOf('\\', indexOfNextSeparator+1);
            if (indexOfWinSeparator < 0)
                indexOfWinSeparator = fileNameLength;
        }

        if (i != indexOfNextSeparator) // Skip empty parts (e.g. '//')
            list << mFileName.mid(i, indexOfNextSeparator-i);
        i = indexOfNextSeparator + 1;

    } while (i < fileNameLength);

    return list;
}

//! Creates a path string by joining a list of directories/filename
/*! The path can be constructed from the entire list of path components
 *  (directories/filenames) or from just a subset. The suitable directory
 *  separators will be inserted as needed.
 *  \param parts the components of the path. Each entry in the list should be a child
 *          directory (or file) to the directory named in the preceeding entry.
 *  \param pos the index of the first path component to use.
 *  \param length the number of path components to include in the returned path.
 *          If \c -1 then all components from \a pos will be included.
 *  \note If one of the \a parts starts with a root prefix (e.g. \c "\" or \c "/"
 *          then the preceeding parts will be discarded and the parts anchored
 *          from that root. */
QString PathString::joinPath(const QStringList& parts, const int pos, const int length)
{
    if (parts.isEmpty())
        return QString{};
    QString path;
    const int end = length < 0? parts.size()-pos : qMin(pos+length, parts.size()-pos);

    // Avoid repeatedly resizing the 'path' string by reserving
    // enough space for all the filename parts, plus additional
    // separators.
    int reserveSize = 0;
    for (int i=pos; i<end; ++i)
        reserveSize += parts.at(i).size() + 1;
    path.reserve(reserveSize);

    // Combine all the entries in 'parts', adding file/directory
    // separators as needed.
    for (int i=pos; i<end; ++i)
    {
        if (path.isNull())
            path = parts.at(i);
        else
        {
            const QString& part = parts.at(i);
            if (hasUnixRoot(part) ||
                hasWindowsRoot(part) ||
                hasUncPrefix(part))
                path = part;
            else
            {
                if (!endsWithSlash(path))
                    path += separator(path);
                path += part;
            }
        }
    }
    return path;
}

// Adapted from Qt 4.7.4 QDir::cleanPath.
// Accepts both Unix and Windows style paths.
//! Clean-up a file name.
//! \todo FIXME: This needs a better description!
QString PathString::cleanPath(const QString &path)
{
    if (path.isEmpty())
        return path;
    QString name = path;

    int used = 0, levels = 0;
    const int len = name.length();
    QVarLengthArray<QChar> outVector(len);
    QChar *out = outVector.data();

    const QChar *p = name.unicode();
    for (int i = 0, last = -1, iwrite = 0; i < len; ++i) {
        if (isSlash(p[i])) {
            while (i < len-1 && isSlash(p[i+1])) {
                if (!i)
                    break;
                i++;
            }
            bool eaten = false;
            if (i < len - 1 && p[i+1] == QLatin1Char('.')) {
                int dotcount = 1;
                if (i < len - 2 && p[i+2] == QLatin1Char('.'))
                    dotcount++;
                if (i == len - dotcount - 1) {
                    if (dotcount == 1) {
                        break;
                    } else if (levels) {
                        if (last == -1) {
                            for (int i2 = iwrite-1; i2 >= 0; i2--) {
                                if (isSlash(out[i2])) {
                                    last = i2;
                                    break;
                                }
                            }
                        }
                        used -= iwrite - last - 1;
                        break;
                    }
                } else if (isSlash(p[i+dotcount+1])) {
                    if (dotcount == 2 && levels) {
                        if (last == -1 || iwrite - last == 1) {
                            for (int i2 = (last == -1) ? (iwrite-1) : (last-1); i2 >= 0; i2--) {
                                if (isSlash(out[i2])) {
                                    eaten = true;
                                    last = i2;
                                    break;
                                }
                            }
                        } else {
                            eaten = true;
                        }
                        if (eaten) {
                            levels--;
                            used -= iwrite - last;
                            iwrite = last;
                            last = -1;
                        }
                    } else if (dotcount == 2 && i > 0 && p[i - 1] != QLatin1Char('.')) {
                        eaten = true;
                        used -= iwrite - qMax(0, last);
                        iwrite = qMax(0, last);
                        last = -1;
                        ++i;
                    } else if (dotcount == 1) {
                        eaten = true;
                    }
                    if (eaten)
                        i += dotcount;
                } else {
                    levels++;
                }
            } else if (last != -1 && iwrite - last == 1) {
                eaten = (iwrite > 2);
                last = -1;
            } else if (last != -1 && i == len-1) {
                eaten = true;
            } else {
                levels++;
            }
            if (!eaten)
                last = i - (i - iwrite);
            else
                continue;
        } else if (!i && p[i] == QLatin1Char('.')) {
            int dotcount = 1;
            if (len >= 1 && p[1] == QLatin1Char('.'))
                dotcount++;
            if (len >= dotcount && isSlash(p[dotcount])) {
                if (dotcount == 1) {
                    i++;
                    while (i+1 < len-1 && isSlash(p[i+1]))
                        i++;
                    continue;
                }
            }
        }
        out[iwrite++] = p[i];
        used++;
    }

    // If first character is root return root. This is to fix a
    // bug where "/." or "/./." would return as the empty string.
    // See https://bugreports.qt.io/browse/QTBUG-12125
    if (!used && hasUnixRoot(name))
        return name.at(0);

    QString ret = (used == len ? name : QString(out, used));
    // Strip away last slash except for root directories
    if (ret.length() > 1 && endsWithSlash(ret)) {
        if (!(ret.length() == 3 && ret.at(1) == QLatin1Char(':')))
            ret.chop(1);
    }

    return ret;
}

//! Compare two file names, considering relative paths (but not symbolic links).
/*!
 * \param  f1,f2  The file names to compare.
 *
 * \return  `true` if the file names refer to the same file, `false` otherwise.
 */
bool
PathString::fileNamesEqual (QString const &f1, QString  const &f2)
{
    // Check for simple string equality before doing (relatively expensive) path-cleaning
    if (f1 == f2)
        return true;

    // Check for empty file names.
    {
        bool const f1empty = f1.isEmpty (),
                   f2empty = f2.isEmpty ();
        if (f1empty || f2empty)
            return f1empty && f2empty;
    }

    // Compare cleaned-up file names as strings.
    PathString const p1 (cleanPath (f1)),
                     p2 (cleanPath (f2));
    if (p1.mFileName == p2.mFileName)
        return true;

    // If they differ and are both absolute, game over.
    // (Unless decide to follow symbolic links!)
    if (p1.isAbsolute () && p2.isAbsolute ())
        return false;

    // Compare file names, level-by-level, allowing for relative paths.
    QString const parent = QLatin1String("..");
    QStringList l1 = p1.splitPath (),
                l2 = p2.splitPath ();
    QStringList::const_iterator const begin1 = l1.constBegin (),
                                      begin2 = l2.constBegin ();
    QStringList::const_iterator i1 = l1.constEnd (),
                                i2 = l2.constEnd ();
    while (i1 != begin1 && i2 != begin2)
    {
        --i1, --i2;
        QString const &c1 = *i1,
                      &c2 = *i2;
        if (c1 != c2 && c1 != parent && c2 != parent)
            return false;
    }
    return true;
}
