// MobileGL - MobileGL/ConfigLoader.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
//   https://www.gnu.org/licenses/gpl-3.0.txt
//   https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header

#include "Config.h"

#include <cerrno>
#include <cstdlib>

#ifndef _WIN32
extern char** environ;
#endif

namespace MobileGL::MG_Config {
    // Zero/default-initialized at static-init time (all fields have constexpr-friendly
    // defaults), so it is safe to read even if MG_ConfigLoader::Init has not run yet.
    FeaturesTable Features;
} // namespace MobileGL::MG_Config

namespace MobileGL::MG_ConfigLoader {
    static UniquePtr<UnorderedMap<String, String>> acceptedEnvVariablesMap;

    static Bool IsAcceptedPrefix(const String& key) {
        return (key.compare(0, 6, "LIBGL_") == 0 || key.compare(0, 9, "MOBILEGL_") == 0);
    }

    inline void InitializeAcceptedEnvVariables() {
        if (!acceptedEnvVariablesMap) {
            acceptedEnvVariablesMap = MakeUnique<UnorderedMap<String, String>>();
        } else {
            acceptedEnvVariablesMap->clear();
        }

        char** envPtr = nullptr;

#ifdef _WIN32
        envPtr = _environ;
#else // POSIX
        envPtr = ::environ;
#endif

        if (envPtr == nullptr) return;

        for (char** env = envPtr; *env != nullptr; ++env) {
            String entry(*env);
            SizeT pos = entry.find('=');
            if (pos != String::npos) {
                String key = entry.substr(0, pos);
                String value = entry.substr(pos + 1);

                if (IsAcceptedPrefix(key)) {
                    (*acceptedEnvVariablesMap)[key] = value;
                    MGLOG_D("Config: Accepted env variable: %s=%s", key.c_str(), value.c_str());
                }
            }
        }
    }

    inline void QueryEnvVariable(const String& key, String& outValue, const String& defaultValue) {
        auto it = acceptedEnvVariablesMap->find(key);
        if (it != acceptedEnvVariablesMap->end()) {
            outValue = it->second;
        } else {
            outValue = defaultValue;
        }
    }

    // Unified truthy rule for boolean feature env variables: set, non-empty, not "0",
    // and not "false" (case-insensitive).
    static Bool IsTruthyValue(const String& value) {
        if (value.empty() || value == "0") {
            return false;
        }
        String lowered = value;
        std::transform(lowered.begin(), lowered.end(), lowered.begin(),
                       [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
        return lowered != "false";
    }

    inline Bool QueryEnvFlag(const String& key) {
        auto it = acceptedEnvVariablesMap->find(key);
        return it != acceptedEnvVariablesMap->end() && IsTruthyValue(it->second);
    }

    // Quirk overrides are tri-state: an unset variable keeps device auto-detection, a truthy
    // value forces the quirk on, anything else set ("0", "false", "") forces it off.
    inline MG_Config::QuirkOverride QueryEnvQuirkOverride(const String& key) {
        auto it = acceptedEnvVariablesMap->find(key);
        if (it == acceptedEnvVariablesMap->end()) {
            return MG_Config::QuirkOverride::Auto;
        }
        return IsTruthyValue(it->second) ? MG_Config::QuirkOverride::ForceOn
                                         : MG_Config::QuirkOverride::ForceOff;
    }

    // Multi-draw mode is a named-value preference: unset keeps Auto (best supported tier),
    // a recognized name selects that tier as the ceiling, anything else warns and keeps Auto.
    inline MG_Config::MultiDrawMode QueryEnvMultiDrawMode(const String& key) {
        auto it = acceptedEnvVariablesMap->find(key);
        if (it == acceptedEnvVariablesMap->end()) {
            return MG_Config::MultiDrawMode::Auto;
        }
        String lowered = it->second;
        std::transform(lowered.begin(), lowered.end(), lowered.begin(),
                       [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
        if (lowered == "ext") return MG_Config::MultiDrawMode::Ext;
        if (lowered == "indirect") return MG_Config::MultiDrawMode::Indirect;
        if (lowered == "unroll") return MG_Config::MultiDrawMode::Unroll;
        if (lowered.empty() || lowered == "auto") return MG_Config::MultiDrawMode::Auto;
        MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected ext|indirect|unroll|auto, using auto",
                key.c_str(), it->second.c_str());
        return MG_Config::MultiDrawMode::Auto;
    }

    // Same contract as QueryEnvMultiDrawMode, over the DirectGLES tier names.
    inline MG_Config::GLESMultiDrawMode QueryEnvGLESMultiDrawMode(const String& key) {
        auto it = acceptedEnvVariablesMap->find(key);
        if (it == acceptedEnvVariablesMap->end()) {
            return MG_Config::GLESMultiDrawMode::Auto;
        }
        String lowered = it->second;
        std::transform(lowered.begin(), lowered.end(), lowered.begin(),
                       [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
        if (lowered == "ext") return MG_Config::GLESMultiDrawMode::Ext;
        if (lowered == "multiindirect") return MG_Config::GLESMultiDrawMode::MultiIndirect;
        if (lowered == "indirect") return MG_Config::GLESMultiDrawMode::Indirect;
        if (lowered == "basevertex") return MG_Config::GLESMultiDrawMode::BaseVertex;
        if (lowered == "drawelements") return MG_Config::GLESMultiDrawMode::DrawElements;
        if (lowered == "compute") return MG_Config::GLESMultiDrawMode::Compute;
        if (lowered.empty() || lowered == "auto") return MG_Config::GLESMultiDrawMode::Auto;
        MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected "
                "ext|multiindirect|indirect|basevertex|drawelements|compute|auto, using auto",
                key.c_str(), it->second.c_str());
        return MG_Config::GLESMultiDrawMode::Auto;
    }

    inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
        auto it = acceptedEnvVariablesMap->find(key);
        if (it == acceptedEnvVariablesMap->end()) {
            return defaultValue;
        }

        const String& value = it->second;
        char* parseEnd = nullptr;
        errno = 0;
        const unsigned long parsedValue = std::strtoul(value.c_str(), &parseEnd, 10);
        if (parseEnd == value.c_str() || *parseEnd != '\0' || errno == ERANGE || parsedValue < minValue ||
            parsedValue > maxValue) {
            MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected an integer in range [%u, %u], "
                    "using default %u",
                    key.c_str(), value.c_str(), minValue, maxValue, defaultValue);
            return defaultValue;
        }

        return static_cast<Uint32>(parsedValue);
    }

    inline void InitFeatures() {
        auto& features = MG_Config::Features;
        features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
        features.EnableGlesTextureView = QueryEnvFlag("MOBILEGL_ENABLE_GLES_TEXTURE_VIEW");
        features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
        features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
        QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
#endif
        features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
        features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
        features.FixIterationRPSubgroupScratch =
            QueryEnvQuirkOverride("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
        features.IterationRPFixBarrier = QueryEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
        features.DeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_DERIVE_NUM_SUBGROUPS");
        features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
        features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
        features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
        features.AvoidSamplerMipmapMinFilter =
            QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
        features.AvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_AVOID_EXPLICIT_LOD_BIAS");
        features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
        features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
        features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
        features.DisableUnpackRing = QueryEnvFlag("MOBILEGL_DISABLE_UNPACK_RING");
        features.EsprytForceDepthStencilReadbackEmulation =
            QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
        features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
        features.MagmaDisableBlendedDepthWriteQuirk =
            QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
        features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
        features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
        features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
        features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
        features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
        features.AsyncOptimisticShaderStatus =
            QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
        features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
        features.ViewportArrayEmulation =
            QueryEnvQuirkOverride("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION");
    }

    inline void InitBackendType() {
        String backendTypeStr;
        QueryEnvVariable("MOBILEGL_BACKEND_TYPE", backendTypeStr, "DirectGLES");
#define ENTRY(backendType)                                                                                             \
    if (backendTypeStr == #backendType) {                                                                              \
        MG_Config::ActiveBackendType = BackendType::backendType;                                                       \
        MGLOG_I("Config: Active backend type set to " #backendType);                                                   \
        return;                                                                                                        \
    }
        ENTRY(DirectGLES)
        ENTRY(DirectVulkan)
        ENTRY(Unknown)
        MG_Config::ActiveBackendType = BackendType::Unknown;
#undef ENTRY
    }

    void Init() {
        MGLOG_D("Loading configuration from environment variables...");
        InitializeAcceptedEnvVariables();

        InitBackendType();
        InitFeatures();

        // Destroy the map since we won't need it anymore
        acceptedEnvVariablesMap.reset();
    }
} // namespace MobileGL::MG_ConfigLoader
