// 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 "utilities.h"

#include "inode.h"
#include "location.h"
#include "textutils.h"

#include <QtCore/qfileinfo.h>
#include <QtCore/qprocess.h>

QT_BEGIN_NAMESPACE

/*!
    \namespace Utilities
    \internal
    \brief This namespace holds QDoc-internal utility methods.
 */
namespace Utilities {
static inline void setDebugEnabled(bool value)
{
    const_cast<QLoggingCategory &>(lcQdoc()).setEnabled(QtDebugMsg, value);
    const_cast<QLoggingCategory &>(lcQdocClang()).setEnabled(QtDebugMsg, value);
}

void startDebugging(const QString &message)
{
    setDebugEnabled(true);
    qCDebug(lcQdoc, "START DEBUGGING: %ls", qUtf16Printable(message));
}

void stopDebugging(const QString &message)
{
    qCDebug(lcQdoc, "STOP DEBUGGING: %ls", qUtf16Printable(message));
    setDebugEnabled(false);
}

bool debugging()
{
    return lcQdoc().isEnabled(QtDebugMsg);
}

/*!
    \brief Converts a string representation of a pointer address to an INode pointer.

    This function takes a \a string, assumed to contain the numerical
    representation of an INode pointer's address (as generated by
    stringForNode()), and casts it back to an \c INode pointer.

    \sa stringForNode()
 */
const INode *nodeForString(const QString &string)
{
    return reinterpret_cast<const INode *>(string.toULongLong());
}
/*!
    \brief Converts an INode pointer address to its string representation.

    This function takes a \a node pointer and returns a string that contains the
    numerical value of its memory address. This is used for serialization or
    passing node references where a direct pointer cannot be used.

    \note The returned string is only valid within the same process. It does
          \e {not} persist across runs.
 */
QString stringForNode(const INode *node)
{
    return QString::number(reinterpret_cast<quintptr>(node));
}

/*!
    Returns a unique identifier based on location \a loc, with a
    given \a prefix.
*/
QString uniqueIdentifier(const Location &loc, const QString &prefix)
{
    Q_ASSERT(!loc.filePath().isEmpty());
    QFileInfo fi{loc.filePath()};
    const auto id = QLatin1String("%1_%2_%3").arg(prefix, fi.fileName(), QString::number(loc.lineNo()));
    return TextUtils::asAsciiPrintable(id);
}

/*!
    \internal
*/
static bool runProcess(const QString &program, const QStringList &arguments,
                       QByteArray *stdOutIn, QByteArray *stdErrIn)
{
    QProcess process;
    process.start(program, arguments, QProcess::ReadWrite);
    if (!process.waitForStarted()) {
        qCDebug(lcQdoc).nospace() << "Unable to start " << process.program()
                                  << ": " << process.errorString();
        return false;
    }
    process.closeWriteChannel();
    const bool finished = process.waitForFinished();
    const QByteArray stdErr = process.readAllStandardError();
    if (stdErrIn)
        *stdErrIn = stdErr;
    if (stdOutIn)
        *stdOutIn = process.readAllStandardOutput();

    if (!finished) {
        qCDebug(lcQdoc).nospace() << process.program() << " timed out: " << stdErr;
        process.kill();
        return false;
    }

    if (process.exitStatus() != QProcess::NormalExit) {
        qCDebug(lcQdoc).nospace() << process.program() << " crashed: " << stdErr;
        return false;
    }

    if (process.exitCode() != 0) {
        qCDebug(lcQdoc).nospace() <<  process.program() << " exited with "
            << process.exitCode() << ": " << stdErr;
        return false;
    }

    return true;
}

/*!
    \internal
*/
static QByteArray frameworkSuffix() {
    return QByteArrayLiteral(" (framework directory)");
}

/*!
    \internal
    Determine the compiler's internal include paths from the output of

    \badcode
    [clang++|g++] -E -x c++ - -v </dev/null
    \endcode

    Output looks like:

    \badcode
    #include <...> search starts here:
    /usr/local/include
    /System/Library/Frameworks (framework directory)
    End of search list.
    \endcode
*/
QStringList getInternalIncludePaths(const QString &compiler)
{
    QStringList result;
    QStringList arguments;
    arguments << QStringLiteral("-E") << QStringLiteral("-x") << QStringLiteral("c++")
              << QStringLiteral("-") << QStringLiteral("-v");
    QByteArray stdOut;
    QByteArray stdErr;
    if (!runProcess(compiler, arguments, &stdOut, &stdErr))
        return result;
    const QByteArrayList stdErrLines = stdErr.split('\n');
    bool isIncludeDir = false;
    for (const QByteArray &line : stdErrLines) {
        if (isIncludeDir) {
            if (line.startsWith(QByteArrayLiteral("End of search list"))) {
                isIncludeDir = false;
            } else {
                QByteArray prefix("-I");
                QByteArray headerPath{line.trimmed()};
                if (headerPath.endsWith(frameworkSuffix())) {
                    headerPath.truncate(headerPath.size() - frameworkSuffix().size());
                    prefix = QByteArrayLiteral("-F");
                }
                result.append(QString::fromLocal8Bit(prefix + headerPath));
            }
        } else if (line.startsWith(QByteArrayLiteral("#include <...> search starts here"))) {
            isIncludeDir = true;
        }
    }

    return result;
}

bool isGeneratedFile(const QString &path)
{
    QString fileName = QFileInfo(path).fileName();
    return fileName.startsWith("moc_") ||
           fileName.startsWith("qrc_") ||
           fileName.startsWith("ui_");
}

/*!
    Returns a string list containing the path and fragment components of the
    given \a linkText. Fragments begin with a "#" character, following HTML
    conventions.

    Links with only a path or only a fragment will result in lists of one
    element. A link with both path and fragment will result in a list of two
    elements. Additional fragments are discarded.

    Escaped "#" characters ("\\#") are interpreted differently to regular "#"
    characters. These can be used to represent "#" characters in titles, but
    not in fragments.
*/
QStringList pathAndFragment(const QString &linkText)
{
    QStringList pieces = linkText.split("\\#"_L1);
    QStringList result;
    QString next;

    for (auto &piece : pieces) {
        QStringList fragmentPieces = piece.split('#');
        next += fragmentPieces.takeFirst();
        if (!fragmentPieces.isEmpty()) {
            result.append(next);
            next = fragmentPieces.first();
        }
        if (result.count() == 2)
            break;
    }
    if (!next.isEmpty() && result.count() < 2)
        result.append(next);

    return result;
}

/*!
    Constructs an href link from an example file name.

    The \a path is the path to the example file. The \a project is the
    module name (used as prefix). The \a fileExt is the file extension
    for the generated link.
 */
QString linkForExampleFile(const QString &path, const QString &project, const QString &fileExt)
{
    QString link = project.toLower() + QLatin1Char('-') + path;
    return TextUtils::asAsciiPrintable(link) + QLatin1Char('.') + fileExt;
}

/*!
    Constructs a title for a file or image page in an example.

    Returns the base name of \a fileName with the appropriate suffix
    based on \a kind: " Example File" for ExampleFileKind::File,
    " Image File" for ExampleFileKind::Image.

    Use this overload when you already know whether the file is an
    example file or image (e.g., when iterating separate file/image lists).
 */
QString exampleFileTitle(const QString &fileName, ExampleFileKind kind)
{
    const QString suffix = [kind]() {
        switch (kind) {
        case ExampleFileKind::File:
            return " Example File"_L1;
        case ExampleFileKind::Image:
            return " Image File"_L1;
        }
        Q_UNREACHABLE();
    }();
    return QFileInfo(fileName).fileName() + suffix;
}

/*!
    Constructs a title for a file or image page in an example.

    If \a fileName is found in \a files, returns the base name with
    " Example File" suffix. If found in \a images, returns the base
    name with " Image File" suffix. If not found in either list,
    returns an empty string.

    Matching uses exact string equality, not basename comparison.

    \note This overload performs O(n) membership checks. When iterating
    known file or image lists, prefer the overload taking ExampleFileKind.
 */
QString exampleFileTitle(const QStringList &files, const QStringList &images, const QString &fileName)
{
    if (files.contains(fileName))
        return exampleFileTitle(fileName, ExampleFileKind::File);
    if (images.contains(fileName))
        return exampleFileTitle(fileName, ExampleFileKind::Image);
    return {};
}

} // namespace Utilities

QT_END_NAMESPACE
