#include "javalocator.h"

#include "javaruntime.h"

#include <QDir>
#include <QDirIterator>
#include <QFileInfo>
#include <QHash>
#include <QProcess>
#include <QProcessEnvironment>
#include <QObject>
#include <QRegularExpression>
#include <QStandardPaths>
#include <utility>

namespace {

#if defined(Q_OS_WIN)
constexpr auto kExecutable = "java.exe";
constexpr auto kWindowless = "javaw.exe";
#else
constexpr auto kExecutable = "java";
constexpr auto kWindowless = "java";
#endif

// Корни, в которых Windows/Linux/macOS обычно держат JDK и JRE.
QStringList installationRoots()
{
    QStringList roots;
#if defined(Q_OS_WIN)
    const QStringList programFiles = {qEnvironmentVariable("ProgramFiles"),
                                      qEnvironmentVariable("ProgramFiles(x86)"),
                                      qEnvironmentVariable("ProgramW6432")};
    const QStringList vendors = {QStringLiteral("Java"),
                                 QStringLiteral("Eclipse Adoptium"),
                                 QStringLiteral("Eclipse Foundation"),
                                 QStringLiteral("AdoptOpenJDK"),
                                 QStringLiteral("Amazon Corretto"),
                                 QStringLiteral("Zulu"),
                                 QStringLiteral("BellSoft"),
                                 QStringLiteral("Microsoft"),
                                 QStringLiteral("Semeru"),
                                 QStringLiteral("Android/Android Studio/jbr")};
    for (const QString &base : programFiles) {
        if (base.isEmpty())
            continue;
        for (const QString &vendor : vendors)
            roots << base + u'/' + vendor;
    }
#elif defined(Q_OS_MACOS)
    roots << QStringLiteral("/Library/Java/JavaVirtualMachines")
          << QDir::homePath() + QStringLiteral("/Library/Java/JavaVirtualMachines");
#else
    roots << QStringLiteral("/usr/lib/jvm") << QStringLiteral("/usr/java")
          << QStringLiteral("/opt/java") << QStringLiteral("/opt/jdk");
#endif
    return roots;
}

// Ищем именно java.exe: у javaw.exe нет вывода, а версию мы определяем запуском.
void appendIfExecutable(QStringList &target, const QString &binDir)
{
    const QFileInfo info(binDir + u'/' + QString::fromLatin1(kExecutable));
    if (!info.isFile() || !info.isExecutable())
        return;
    const QString path = info.absoluteFilePath();
    if (!target.contains(path))
        target << path;
}

} // namespace

QStringList JavaLocator::findAll(const QString &gameDir)
{
    QStringList result;

    // 0. Сборки, скачанные самим лаунчером. Идут первыми: пользователь выбирал
    // их осознанно, в отличие от того, что нашлось в системе.
    const QList<InstalledJavaRuntime> managed = JavaRuntimeStore::installed();
    for (const InstalledJavaRuntime &entry : managed) {
        if (!result.contains(entry.javaPath))
            result << entry.javaPath;
    }

    // 1. Рантайм, скачанный самим Minecraft: .minecraft/runtime/<component>/<os>/<component>/bin
    const QString runtime = gameDir + QStringLiteral("/runtime");
    if (QDir(runtime).exists()) {
        QDirIterator it(runtime, QStringList{QString::fromLatin1(kExecutable)},
                        QDir::Files | QDir::Executable, QDirIterator::Subdirectories);
        while (it.hasNext()) {
            it.next();
            appendIfExecutable(result, it.fileInfo().absolutePath());
        }
    }

    // 2. JAVA_HOME
    const QString javaHome = qEnvironmentVariable("JAVA_HOME");
    if (!javaHome.isEmpty())
        appendIfExecutable(result, javaHome + QStringLiteral("/bin"));

    // 3. PATH
    const QString onPath = QStandardPaths::findExecutable(QString::fromLatin1(kExecutable));
    if (!onPath.isEmpty())
        appendIfExecutable(result, QFileInfo(onPath).absolutePath());

    // 4. Типовые каталоги установки
    const QStringList roots = installationRoots();
    for (const QString &root : roots) {
        const QDir dir(root);
        if (!dir.exists())
            continue;
        const QStringList entries = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
        for (const QString &entry : entries) {
            const QString home = dir.absoluteFilePath(entry);
            appendIfExecutable(result, home + QStringLiteral("/bin"));
            appendIfExecutable(result, home + QStringLiteral("/Contents/Home/bin"));   // macOS
            appendIfExecutable(result, home + QStringLiteral("/jre/bin"));
        }
    }

    return result;
}

int JavaLocator::majorVersion(const QString &javaPath)
{
    static QHash<QString, int> cache;
    const auto cached = cache.constFind(javaPath);
    if (cached != cache.constEnd())
        return cached.value();

    int major = 0;
    QProcess process;
    process.start(javaPath, {QStringLiteral("-version")});
    if (process.waitForFinished(5000)) {
        // java печатает версию в stderr: openjdk version "17.0.2" 2022-01-18
        const QString output = QString::fromLocal8Bit(process.readAllStandardError())
                               + QString::fromLocal8Bit(process.readAllStandardOutput());
        static const QRegularExpression re(QStringLiteral("version\\s+\"(\\d+)(?:\\.(\\d+))?"));
        const QRegularExpressionMatch match = re.match(output);
        if (match.hasMatch()) {
            const int first = match.captured(1).toInt();
            // До Java 9 версия выглядела как 1.8.0_301 — мажорная тут вторая цифра.
            major = (first == 1) ? match.captured(2).toInt() : first;
        }
    }

    cache.insert(javaPath, major);
    return major;
}

QString JavaLocator::windowlessVariant(const QString &javaPath)
{
    const QFileInfo info(javaPath);
    if (info.fileName() != QLatin1String(kExecutable))
        return javaPath;
    const QFileInfo windowless(info.absolutePath() + u'/' + QString::fromLatin1(kWindowless));
    return windowless.isFile() ? windowless.absoluteFilePath() : javaPath;
}

QString JavaLocator::select(const QString &gameDir,
                            int requiredMajor,
                            const QString &preferred,
                            QString *error)
{
    QStringList candidates;
    if (!preferred.isEmpty()) {
        QFileInfo info(preferred);
        // В настройках можно указать как сам java.exe, так и корень JDK.
        if (info.isDir())
            appendIfExecutable(candidates, info.absoluteFilePath() + QStringLiteral("/bin"));
        else if (info.isFile())
            candidates << info.absoluteFilePath();

        if (candidates.isEmpty()) {
            if (error)
                *error = QObject::tr("Java по указанному пути не найдена: %1")
                             .arg(QDir::toNativeSeparators(preferred));
            return {};
        }
    }
    candidates += findAll(gameDir);

    if (candidates.isEmpty()) {
        if (error)
            *error = QObject::tr("Java не найдена. Установите JDK %1 или укажите путь в настройках.")
                         .arg(requiredMajor);
        return {};
    }

    QString bestBelow;
    int bestBelowMajor = 0;
    for (const QString &candidate : std::as_const(candidates)) {
        const int major = majorVersion(candidate);
        if (major >= requiredMajor)
            return candidate;
        if (major > bestBelowMajor) {
            bestBelowMajor = major;
            bestBelow = candidate;
        }
    }

    if (error) {
        *error = bestBelowMajor > 0
                     ? QObject::tr("Для этой версии нужна Java %1 или новее, найдена только Java %2 (%3).")
                           .arg(requiredMajor)
                           .arg(bestBelowMajor)
                           .arg(QDir::toNativeSeparators(bestBelow))
                     : QObject::tr("Не удалось определить версию установленной Java. "
                                   "Укажите путь к JDK %1 в настройках.")
                           .arg(requiredMajor);
    }
    return {};
}
