// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later

/**
 * @file native_freedreno.cpp
 * @brief JNI bindings for Freedreno/Turnip GPU driver configuration.
 *
 * Provides runtime configuration of Mesa Freedreno environment variables
 * for the Turnip Vulkan driver on Adreno GPUs.
 *
 * @see https://docs.mesa3d.org/drivers/freedreno.html
 */

#include <algorithm>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <map>
#include <memory>
#include <string>
#include <sys/stat.h>

#include <jni.h>

#include "common/android/android_common.h"
#include "common/logging.h"
#include "native.h"

namespace {

struct FreedrenoConfig {
    std::map<std::string, std::string> env_vars;
    std::string config_file_path;
};

std::unique_ptr<FreedrenoConfig> g_config;
std::string g_base_path;
std::string g_current_program_id;

constexpr const char* kConfigFileName = ".freedreno.conf";
constexpr const char* kPerGameConfigDir = "freedreno_games";

void LogActiveVariables() {
    if (!g_config || g_config->env_vars.empty()) {
        return;
    }
    for (const auto& [key, value] : g_config->env_vars) {
        LOG_INFO(Frontend, "[Freedreno] {}={}", key, value);
    }
}

bool ApplyEnvironmentVariable(const std::string& key, const std::string& value) {
    if (setenv(key.c_str(), value.c_str(), 1) != 0) {
        LOG_ERROR(Frontend, "[Freedreno] Failed to set {}={} (errno: {})", key, value, errno);
        return false;
    }
    return true;
}

void ClearAllEnvironmentVariables() {
    if (!g_config) return;
    for (const auto& [key, value] : g_config->env_vars) {
        unsetenv(key.c_str());
    }
    g_config->env_vars.clear();
}

std::string GetConfigPath() {
    return g_base_path + "/" + kConfigFileName;
}

std::string GetPerGameConfigPath(const std::string& program_id) {
    return g_base_path + "/" + kPerGameConfigDir + "/" + program_id + ".conf";
}

void EnsurePerGameConfigDir() {
    std::string dir_path = g_base_path + "/" + kPerGameConfigDir;
    mkdir(dir_path.c_str(), 0755);
}

bool LoadConfigFromFile(const std::string& config_path) {
    if (!g_config) return false;

    FILE* file = fopen(config_path.c_str(), "r");
    if (!file) {
        return false;
    }

    char line[512];
    int count = 0;
    while (fgets(line, sizeof(line), file)) {
        size_t len = strlen(line);
        if (len > 0 && line[len - 1] == '\n') {
            line[len - 1] = '\0';
            len--;
        }

        if (len == 0 || line[0] == '#') {
            continue;
        }

        const char* eq = strchr(line, '=');
        if (!eq) {
            continue;
        }

        std::string key(line, eq - line);
        std::string value(eq + 1);

        g_config->env_vars[key] = value;
        ApplyEnvironmentVariable(key, value);
        count++;
    }

    fclose(file);
    return count > 0;
}

bool SaveConfigToFile(const std::string& config_path) {
    if (!g_config) return false;

    FILE* file = fopen(config_path.c_str(), "w");
    if (!file) {
        LOG_ERROR(Frontend, "[Freedreno] Failed to open {} for writing", config_path);
        return false;
    }

    fprintf(file, "# Freedreno/Turnip Configuration\n");
    fprintf(file, "# Auto-generated by Eden Emulator\n\n");

    for (const auto& [key, value] : g_config->env_vars) {
        fprintf(file, "%s=%s\n", key.c_str(), value.c_str());
    }

    fclose(file);
    return true;
}

} // anonymous namespace

extern "C" {

JNIEXPORT void JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_setFreedrenoBasePath(
    JNIEnv* env, [[maybe_unused]] jobject obj, jstring jbasePath) {
    g_base_path = Common::Android::GetJString(env, jbasePath);
}

JNIEXPORT void JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_initializeFreedrenoConfig(
    [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
    if (!g_config) {
        g_config = std::make_unique<FreedrenoConfig>();
        LOG_INFO(Frontend, "[Freedreno] Configuration system initialized");
    }
}

JNIEXPORT void JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_saveFreedrenoConfig(
    [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
    if (!g_config) {
        LOG_WARNING(Frontend, "[Freedreno] Cannot save: not initialized");
        return;
    }

    const std::string config_path = GetConfigPath();
    FILE* file = fopen(config_path.c_str(), "w");
    if (!file) {
        LOG_ERROR(Frontend, "[Freedreno] Failed to open {} for writing", config_path);
        return;
    }

    fprintf(file, "# Freedreno/Turnip Configuration\n");
    fprintf(file, "# Auto-generated by Eden Emulator\n\n");

    for (const auto& [key, value] : g_config->env_vars) {
        fprintf(file, "%s=%s\n", key.c_str(), value.c_str());
    }

    fclose(file);
    g_config->config_file_path = config_path;

    LOG_INFO(Frontend, "[Freedreno] Saved {} variables to {}",
             g_config->env_vars.size(), config_path);
}

JNIEXPORT void JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_reloadFreedrenoConfig(
    [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
    if (!g_config) {
        LOG_WARNING(Frontend, "[Freedreno] Cannot reload: not initialized");
        return;
    }

    const std::string config_path = GetConfigPath();
    g_config->env_vars.clear();

    FILE* file = fopen(config_path.c_str(), "r");
    if (!file) {
        LOG_DEBUG(Frontend, "[Freedreno] No config file found at {}", config_path);
        return;
    }

    char line[512];
    while (fgets(line, sizeof(line), file)) {
        // Remove trailing newline
        size_t len = strlen(line);
        if (len > 0 && line[len - 1] == '\n') {
            line[len - 1] = '\0';
            len--;
        }

        // Skip empty lines and comments
        if (len == 0 || line[0] == '#') {
            continue;
        }

        // Parse key=value
        const char* eq = strchr(line, '=');
        if (!eq) {
            continue;
        }

        std::string key(line, eq - line);
        std::string value(eq + 1);

        g_config->env_vars[key] = value;
        ApplyEnvironmentVariable(key, value);
    }

    fclose(file);
    g_config->config_file_path = config_path;

    if (!g_config->env_vars.empty()) {
        LOG_INFO(Frontend, "[Freedreno] Loaded {} variables:", g_config->env_vars.size());
        LogActiveVariables();
    }
}

JNIEXPORT jboolean JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_setFreedrenoEnv(
    JNIEnv* env, [[maybe_unused]] jobject obj, jstring jvarName, jstring jvalue) {
    if (!g_config) {
        return JNI_FALSE;
    }

    auto var_name = Common::Android::GetJString(env, jvarName);
    auto value = Common::Android::GetJString(env, jvalue);

    if (var_name.empty()) {
        return JNI_FALSE;
    }

    g_config->env_vars[var_name] = value;

    if (!ApplyEnvironmentVariable(var_name, value)) {
        return JNI_FALSE;
    }

    LOG_INFO(Frontend, "[Freedreno] Set {}={}", var_name, value);

    if (var_name == "FD_DEV_FEATURES") {
        LOG_INFO(Frontend, "[Freedreno] FD_DEV_FEATURES enabled: {}", value);
    }

    return JNI_TRUE;
}

JNIEXPORT jstring JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_getFreedrenoEnv(
    JNIEnv* env, [[maybe_unused]] jobject obj, jstring jvarName) {
    if (!g_config) {
        return env->NewStringUTF("");
    }

    auto var_name = Common::Android::GetJString(env, jvarName);
    auto it = g_config->env_vars.find(var_name);

    if (it != g_config->env_vars.end()) {
        return env->NewStringUTF(it->second.c_str());
    }

    return env->NewStringUTF("");
}

JNIEXPORT jboolean JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_isFreedrenoEnvSet(
    JNIEnv* env, [[maybe_unused]] jobject obj, jstring jvarName) {
    if (!g_config) {
        return JNI_FALSE;
    }

    auto var_name = Common::Android::GetJString(env, jvarName);
    auto it = g_config->env_vars.find(var_name);

    return (it != g_config->env_vars.end() && !it->second.empty()) ? JNI_TRUE : JNI_FALSE;
}

JNIEXPORT jboolean JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_clearFreedrenoEnv(
    JNIEnv* env, [[maybe_unused]] jobject obj, jstring jvarName) {
    if (!g_config) {
        return JNI_FALSE;
    }

    auto var_name = Common::Android::GetJString(env, jvarName);
    auto it = g_config->env_vars.find(var_name);

    if (it != g_config->env_vars.end()) {
        g_config->env_vars.erase(it);
        unsetenv(var_name.c_str());
        LOG_INFO(Frontend, "[Freedreno] Cleared {}", var_name);
        return JNI_TRUE;
    }

    return JNI_FALSE;
}

JNIEXPORT void JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_clearAllFreedrenoEnv(
    [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
    if (!g_config) {
        return;
    }

    for (const auto& [key, value] : g_config->env_vars) {
        unsetenv(key.c_str());
    }

    size_t count = g_config->env_vars.size();
    g_config->env_vars.clear();

    if (count > 0) {
        LOG_INFO(Frontend, "[Freedreno] Cleared all {} variables", count);
    }
}

JNIEXPORT jstring JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_getFreedrenoEnvSummary(
    JNIEnv* env, [[maybe_unused]] jobject obj) {
    if (!g_config || g_config->env_vars.empty()) {
        return env->NewStringUTF("");
    }

    std::string summary;
    for (const auto& [key, value] : g_config->env_vars) {
        if (!summary.empty()) {
            summary += ",";
        }
        summary += key + "=" + value;
    }

    return env->NewStringUTF(summary.c_str());
}

JNIEXPORT void JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_setCurrentProgramId(
    JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) {
    g_current_program_id = Common::Android::GetJString(env, jprogramId);
}

JNIEXPORT jboolean JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_loadPerGameConfig(
    JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) {
    if (!g_config) {
        return JNI_FALSE;
    }

    auto program_id = Common::Android::GetJString(env, jprogramId);
    if (program_id.empty()) {
        return JNI_FALSE;
    }

    // Clear current environment variables first
    ClearAllEnvironmentVariables();
    g_current_program_id = program_id;

    // Try to load per-game config - do NOT fall back to global
    // Per-game config should start empty if no config exists yet
    std::string per_game_path = GetPerGameConfigPath(program_id);
    if (LoadConfigFromFile(per_game_path)) {
        LOG_INFO(Frontend, "[Freedreno] Loaded per-game config for {}", program_id);
        LogActiveVariables();
        return JNI_TRUE;
    }

    // No per-game config exists - start with empty config
    LOG_INFO(Frontend, "[Freedreno] No per-game config for {}, starting empty", program_id);
    return JNI_FALSE;
}

JNIEXPORT jboolean JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_loadPerGameConfigWithGlobalFallback(
    JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) {
    if (!g_config) {
        return JNI_FALSE;
    }

    auto program_id = Common::Android::GetJString(env, jprogramId);
    if (program_id.empty()) {
        return JNI_FALSE;
    }

    // Clear current environment variables first
    ClearAllEnvironmentVariables();
    g_current_program_id = program_id;

    // Try to load per-game config first
    std::string per_game_path = GetPerGameConfigPath(program_id);
    if (LoadConfigFromFile(per_game_path)) {
        LOG_INFO(Frontend, "[Freedreno] Loaded per-game config for {}", program_id);
        LogActiveVariables();
        return JNI_TRUE;
    }

    // Fall back to global config for emulation
    std::string global_path = GetConfigPath();
    if (LoadConfigFromFile(global_path)) {
        LOG_INFO(Frontend, "[Freedreno] No per-game config for {}, using global for emulation", program_id);
        LogActiveVariables();
    }

    return JNI_FALSE;
}

JNIEXPORT jboolean JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_savePerGameConfig(
    JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) {
    if (!g_config) {
        return JNI_FALSE;
    }

    auto program_id = Common::Android::GetJString(env, jprogramId);
    if (program_id.empty()) {
        return JNI_FALSE;
    }

    EnsurePerGameConfigDir();
    std::string config_path = GetPerGameConfigPath(program_id);

    if (SaveConfigToFile(config_path)) {
        LOG_INFO(Frontend, "[Freedreno] Saved per-game config for {}", program_id);
        return JNI_TRUE;
    }

    return JNI_FALSE;
}

JNIEXPORT jboolean JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_hasPerGameConfig(
    JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) {
    auto program_id = Common::Android::GetJString(env, jprogramId);
    if (program_id.empty()) {
        return JNI_FALSE;
    }

    std::string config_path = GetPerGameConfigPath(program_id);
    FILE* file = fopen(config_path.c_str(), "r");
    if (file) {
        fclose(file);
        return JNI_TRUE;
    }
    return JNI_FALSE;
}

JNIEXPORT jboolean JNICALL
Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_deletePerGameConfig(
    JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) {
    auto program_id = Common::Android::GetJString(env, jprogramId);
    if (program_id.empty()) {
        return JNI_FALSE;
    }

    std::string config_path = GetPerGameConfigPath(program_id);
    if (remove(config_path.c_str()) == 0) {
        LOG_INFO(Frontend, "[Freedreno] Deleted per-game config for {}", program_id);
        return JNI_TRUE;
    }
    return JNI_FALSE;
}

} // extern "C"
