#include "javaruntime.h"

#include "launcherpaths.h"

#include <QCoreApplication>
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include <QSaveFile>

#include <algorithm>

namespace {

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

const QString kMetaFile = QStringLiteral("runtime.json");

QString translate(const char *text)
{
    return QCoreApplication::translate("JavaRuntime", text);
}

} // namespace

QString javaKindKey(JavaRuntimeKind kind)
{
    switch (kind) {
    case JavaRuntimeKind::Mojang: return QStringLiteral("java");
    case JavaRuntimeKind::Jdk:    return QStringLiteral("jdk");
    case JavaRuntimeKind::Jre:    return QStringLiteral("jre");
    }
    return QStringLiteral("jdk");
}

std::optional<JavaRuntimeKind> javaKindFromKey(const QString &key)
{
    if (key == QLatin1String("java"))
        return JavaRuntimeKind::Mojang;
    if (key == QLatin1String("jdk"))
        return JavaRuntimeKind::Jdk;
    if (key == QLatin1String("jre"))
        return JavaRuntimeKind::Jre;
    return std::nullopt;
}

QString javaKindTitle(JavaRuntimeKind kind)
{
    switch (kind) {
    case JavaRuntimeKind::Mojang: return translate("Java");
    case JavaRuntimeKind::Jdk:    return translate("JDK");
    case JavaRuntimeKind::Jre:    return translate("JRE");
    }
    return {};
}

QString JavaRuntimeStore::sanitizeId(const QString &id)
{
    QString out;
    out.reserve(id.size());
    for (const QChar ch : id) {
        const bool safe = ch.isLetterOrNumber() || ch == u'.' || ch == u'-' || ch == u'_';
        out += safe ? ch : u'_';
    }
    // Точки в начале превратили бы папку в скрытую, а «..» — в путь наружу.
    while (out.startsWith(u'.'))
        out.remove(0, 1);
    return out;
}

QString JavaRuntimeStore::dirFor(const QString &id)
{
    const QString safe = sanitizeId(id);
    if (safe.isEmpty())
        return {};
    return LauncherPaths::javaDir() + u'/' + safe;
}

QString JavaRuntimeStore::locateBinary(const QString &rootDir)
{
    if (rootDir.isEmpty() || !QDir(rootDir).exists())
        return {};

    // Кандидатов бывает несколько: в JDK рядом с bin/java лежит ещё и
    // jre/bin/java. Берём самый короткий путь — это корневой интерпретатор.
    QString best;
    QDirIterator it(rootDir, QStringList{QString::fromLatin1(kExecutable)},
                    QDir::Files, QDirIterator::Subdirectories);
    while (it.hasNext()) {
        it.next();
        const QFileInfo info = it.fileInfo();
        if (info.absolutePath().section(u'/', -1) != QLatin1String("bin"))
            continue;
        const QString path = info.absoluteFilePath();
        if (best.isEmpty() || path.length() < best.length())
            best = path;
    }
    return best;
}

std::optional<InstalledJavaRuntime> JavaRuntimeStore::find(const QString &id)
{
    const QString dir = dirFor(id);
    if (dir.isEmpty())
        return std::nullopt;

    QFile file(dir + u'/' + kMetaFile);
    if (!file.open(QIODevice::ReadOnly))
        return std::nullopt;

    const QJsonObject object = QJsonDocument::fromJson(file.readAll()).object();
    InstalledJavaRuntime runtime;
    runtime.id = object.value(QStringLiteral("id")).toString();
    runtime.kind = object.value(QStringLiteral("kind")).toString();
    runtime.major = object.value(QStringLiteral("major")).toInt();
    runtime.version = object.value(QStringLiteral("version")).toString();
    runtime.size = static_cast<qint64>(object.value(QStringLiteral("size")).toDouble());

    // Путь хранится относительным: папку лаунчера могли перенести вместе с
    // профилем пользователя, а абсолютный путь этого не переживает.
    const QString relative = object.value(QStringLiteral("javaPath")).toString();
    runtime.javaPath = relative.isEmpty() ? QString() : QDir(dir).absoluteFilePath(relative);

    if (runtime.id != id || runtime.javaPath.isEmpty() || !QFileInfo::exists(runtime.javaPath))
        return std::nullopt;
    return runtime;
}

QList<InstalledJavaRuntime> JavaRuntimeStore::installed()
{
    QList<InstalledJavaRuntime> result;
    const QDir root(LauncherPaths::javaDir());
    const QStringList entries = root.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
    for (const QString &entry : entries) {
        const auto runtime = find(entry);
        if (runtime)
            result << *runtime;
    }

    std::stable_sort(result.begin(), result.end(),
                     [](const InstalledJavaRuntime &a, const InstalledJavaRuntime &b) {
                         if (a.major != b.major)
                             return a.major > b.major;
                         return a.version > b.version;
                     });
    return result;
}

bool JavaRuntimeStore::writeMeta(const InstalledJavaRuntime &runtime, QString *error)
{
    const QString dir = dirFor(runtime.id);
    if (dir.isEmpty()) {
        if (error)
            *error = translate("недопустимый идентификатор сборки Java");
        return false;
    }

    const QJsonObject object{
        {QStringLiteral("id"), runtime.id},
        {QStringLiteral("kind"), runtime.kind},
        {QStringLiteral("major"), runtime.major},
        {QStringLiteral("version"), runtime.version},
        {QStringLiteral("javaPath"), QDir(dir).relativeFilePath(runtime.javaPath)},
        {QStringLiteral("size"), static_cast<double>(runtime.size)}};

    QSaveFile file(dir + u'/' + kMetaFile);
    if (!file.open(QIODevice::WriteOnly)) {
        if (error)
            *error = translate("не удалось записать %1").arg(QDir::toNativeSeparators(file.fileName()));
        return false;
    }
    const QByteArray payload = QJsonDocument(object).toJson(QJsonDocument::Compact);
    if (file.write(payload) != payload.size() || !file.commit()) {
        file.cancelWriting();
        if (error)
            *error = translate("не удалось записать %1").arg(QDir::toNativeSeparators(file.fileName()));
        return false;
    }
    return true;
}

bool JavaRuntimeStore::remove(const QString &id, QString *error)
{
    const QString dir = dirFor(id);
    // Удаляем только внутри <root>/java: sanitizeId не пропустит «..», но
    // проверка стоит одной строки, а цена ошибки — чужая папка.
    if (dir.isEmpty() || !dir.startsWith(LauncherPaths::javaDir() + u'/')) {
        if (error)
            *error = translate("недопустимый идентификатор сборки Java");
        return false;
    }
    if (!QDir(dir).exists())
        return true;
    if (!QDir(dir).removeRecursively()) {
        if (error)
            *error = translate("не удалось удалить %1").arg(QDir::toNativeSeparators(dir));
        return false;
    }
    return true;
}

int JavaRequirement::minimumFor(const QString &minecraftVersionId)
{
    const QString id = minecraftVersionId.trimmed();
    if (id.isEmpty())
        return 0;

    // Снапшоты — YYwWWa. Ровных границ по неделям нет, но год отвечает на
    // вопрос «какая Java» точнее, чем ничего: 1.17 (Java 16) вышла в 21-м году,
    // 1.18 (Java 17) в конце того же года, 1.20.5 (Java 21) — в 24-м.
    static const QRegularExpression snapshot(QStringLiteral("^(\\d{2})w\\d{2}[a-z]$"));
    const QRegularExpressionMatch snapshotMatch = snapshot.match(id);
    if (snapshotMatch.hasMatch()) {
        const int year = snapshotMatch.captured(1).toInt();
        if (year >= 24)
            return 21;
        if (year >= 22)
            return 17;
        if (year == 21)
            return 16;
        return 8;
    }

    // Релизы и их пре-релизы: 1.20.5, 1.21, 1.21.4-pre1, 26.2.
    static const QRegularExpression release(
        QStringLiteral("^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?"));
    const QRegularExpressionMatch releaseMatch = release.match(id);
    if (!releaseMatch.hasMatch())
        return 8;   // a1.2.6, b1.7.3, rd-132211 и прочая доисторическая ваниль

    const int epoch = releaseMatch.captured(1).toInt();
    const int major = releaseMatch.captured(2).toInt();
    const int patch = releaseMatch.captured(3).toInt();

    // Нумерация после 1.21.x пошла по годам (26.2) — это заведомо новее 1.20.5.
    if (epoch != 1)
        return 21;
    if (major >= 21)
        return 21;
    if (major == 20)
        return patch >= 5 ? 21 : 17;
    if (major >= 18)
        return 17;
    if (major == 17)
        return 16;
    return 8;
}

QString JavaRequirement::coverage(int javaMajor)
{
    if (javaMajor >= 21)
        return translate("Minecraft 1.20.5 и новее");
    if (javaMajor >= 17)
        return translate("Minecraft 1.18 — 1.20.4");
    if (javaMajor == 16)
        return translate("Minecraft 1.17 — 1.17.1");
    if (javaMajor >= 9)
        return translate("для Minecraft не подходит");
    if (javaMajor > 0)
        return translate("Minecraft 1.16.5 и старше");
    return {};
}
