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

#include "qtsupportutils.h"

#include <utils/algorithm.h>
#include <utils/environment.h>
#include <utils/filepath.h>
#include <utils/qtcassert.h>
#include <utils/qtcprocess.h>

#include <QRegularExpression>

using namespace Utils;
using namespace std::chrono_literals;

namespace QtSupport {
namespace Internal {

static QStringList possibleQMakeCommands()
{
    // On Windows it is always "qmake.exe"
    // On Unix some distributions renamed qmake with a postfix to avoid clashes
    // On OS X, Qt 4 binary packages also has renamed qmake. There are also symbolic links that are
    // named "qmake", but the file dialog always checks against resolved links (native Cocoa issue)
    // Qt 6 CMake built targets, such as Android, are dependent on the host installation
    // and use a script wrapper around the host qmake executable
    // Remote build configurations (e.g. Windows host + Linux target) must be taken into account too
    return {"qmake*", "qtpaths*"};
}

QString qtVersionForQMake(const FilePath &qmakePath)
{
    if (qmakePath.isEmpty())
        return QString();

    Process qmake;
    qmake.setEnvironment(qmakePath.deviceEnvironment());
    qmake.setCommand({qmakePath, {"--version"}});
    qmake.runBlocking(5s);
    if (qmake.result() != ProcessResult::FinishedWithSuccess) {
        qWarning() << qmake.verboseExitMessage();
        return QString();
    }

    const QString output = qmake.allOutput();
    static const QRegularExpression regexp("(QMake version:?)[\\s]*([\\d.]*)",
                                           QRegularExpression::CaseInsensitiveOption);
    const QRegularExpressionMatch match = regexp.match(output);
    const QString qmakeVersion = match.captured(2);
    if (qmakeVersion.startsWith(QLatin1String("2."))
        || qmakeVersion.startsWith(QLatin1String("3."))) {
        static const QRegularExpression regexp2("Using Qt version[\\s]*([\\d\\.]*)",
                                                QRegularExpression::CaseInsensitiveOption);
        const QRegularExpressionMatch match2 = regexp2.match(output);
        const QString version = match2.captured(1);
        return version;
    }
    return QString();
}

static bool isQmake(FilePath path)
{
    if (!path.isExecutableFile())
        return false;
    if (isQtChooser(path))
        path = qtChooserToQmakePath(path.symLinkTarget());
    if (!path.exists())
        return false;
    return !qtVersionForQMake(path).isEmpty();
}

static FilePaths findQmakesInDir(const FilePath &dir)
{
    if (dir.isEmpty())
        return {};

    // Prefer qmake-qt5 to qmake-qt4 by sorting the filenames in reverse order.
    FilePaths qmakes;
    std::set<FilePath> canonicalQmakes;
    const FilePaths candidates = dir.dirEntries(
        {possibleQMakeCommands(), DirFilterFlag::Files}, DirSortFlag::Name | DirSortFlag::Reversed);

    const auto probablyMatchesExistingQmake = [&](const FilePath &qmake) {
        // This deals with symlinks.
        if (!canonicalQmakes.insert(qmake).second)
            return true;

        // Symlinks have high potential for false positives in file size check, so skip that one.
        if (qmake.isSymLink())
            return false;

        // This deals with the case where copies are shipped instead of symlinks
        // (Windows, Qt installer, ...).
        return contains(qmakes, [size = qmake.fileSize()](const FilePath &existing) {
            return existing.fileSize() == size;
        });
    };

    for (const FilePath &candidate : candidates) {
        if (isQmake(candidate) && !probablyMatchesExistingQmake(candidate))
            qmakes << candidate;
    }
    return qmakes;
}

FilePaths findQtsInEnvironment(const Environment &env, const FilePath &deviceRoot)
{
    return findQtsInPaths(Utils::transform(env.path(), [&deviceRoot](const FilePath &path) {
        FilePath devPath = deviceRoot.withNewMappedPath(path);
        if (QTC_UNEXPECTED(deviceRoot.isEmpty()))
            devPath = path;
        return devPath;
    }));
}

FilePaths findQtsInPaths(const Utils::FilePaths &paths)
{
    FilePaths qmakeList;
    std::set<FilePath> canonicalEnvPaths;
    for (const FilePath &path : paths) {
        if (!canonicalEnvPaths.insert(path.canonicalPath()).second)
            continue;
        qmakeList << findQmakesInDir(path);
    }
    return qmakeList;
}

bool isQtChooser(const FilePath &filePath)
{
    if (filePath.isSymLink())
        return filePath.symLinkTarget().endsWith("/qtchooser");
    return filePath.endsWith("/qtchooser");
}

FilePath qtChooserToQmakePath(const Utils::FilePath &qtChooser)
{
    FilePath resolvedQtChooser = qtChooser;
    if (qtChooser.isSymLink())
        resolvedQtChooser = qtChooser.resolveSymlinks();

    const QString toolDir = QLatin1String("QTTOOLDIR=\"");
    Process proc;
    proc.setEnvironment(resolvedQtChooser.deviceEnvironment());
    proc.setCommand({resolvedQtChooser, {"-print-env"}});
    proc.runBlocking(1s);
    if (proc.result() != ProcessResult::FinishedWithSuccess)
        return {};
    const QString output = proc.cleanedStdOut();
    int pos = output.indexOf(toolDir);
    if (pos == -1)
        return {};
    pos += toolDir.size();
    int end = output.indexOf('\"', pos);
    if (end == -1)
        return {};

    return resolvedQtChooser.withNewPath(output.mid(pos, end - pos) + "/qmake");
}

} // namespace Internal

QString filterForQmakeFileDialog()
{
    QString filter = QLatin1String("qmake (");
    const QStringList commands = Internal::possibleQMakeCommands();
    for (int i = 0; i < commands.size(); ++i) {
        if (i)
            filter += QLatin1Char(' ');
        filter += commands.at(i);
        if (HostOsInfo::isAnyUnixHost() && !HostOsInfo::isMacHost())
            // kde bug, we need at least one wildcard character
            // see QTCREATORBUG-7771
            filter += QLatin1Char('*');
    }
    filter += QLatin1Char(')');
    return filter;
}

} // namespace QtSupport
