/*
 * Copyright 2022 Rive
 */

#include "testing_window.hpp"

#include "rive/rive_types.hpp"
#include <string>
#include <sstream>

#ifdef _WIN32
#include <windows.h>
#endif

#if defined(_WIN32) && !defined(RIVE_UNREAL)
extern "C"
{
    // https://stackoverflow.com/questions/68469954/how-to-choose-specific-gpu-when-create-opengl-context:
    //
    //   OpenGL, or rather the Win32 GDI integration of it, doesn't offer means
    //   to explicitly select the desired device. However the drivers of Nvidia
    //   and AMD offer a workaround to have programs select, that they prefer to
    //   execute on the discrete GPU rather than the CPU integrated one.
    //
    // These also appear to select the discrete "Arc" GPU on an Intel system,
    // and to influence the GPU selection on D3D11.
    __declspec(dllexport) uint32_t NvOptimusEnablement = 1;
    __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
}
#endif

// Call TestingWindow::Destroy if you want to delete the window singleton
TestingWindow* s_TestingWindow = nullptr;

const char* TestingWindow::BackendName(Backend backend)
{
    switch (backend)
    {
        case TestingWindow::Backend::gl:
            return "gl";
        case TestingWindow::Backend::d3d:
            return "d3d";
        case TestingWindow::Backend::d3d12:
            return "d3d12";
        case TestingWindow::Backend::metal:
            return "metal";
        case TestingWindow::Backend::vk:
            return "vk";
        case TestingWindow::Backend::moltenvk:
            return "moltenvk";
        case TestingWindow::Backend::swiftshader:
            return "swiftshader";
        case TestingWindow::Backend::angle:
            return "angle";
        case TestingWindow::Backend::dawn:
            return "dawn";
        case TestingWindow::Backend::wgpu:
            return "wgpu";
        case Backend::rhi:
            return "rhi";
        case TestingWindow::Backend::coregraphics:
            return "coregraphics";
        case TestingWindow::Backend::skia:
            return "skia";
        case TestingWindow::Backend::null:
            return "null";
    }
    RIVE_UNREACHABLE();
}

static std::vector<std::string> split(const char* str, char delimiter)
{
    std::stringstream ss(str);
    std::string token;
    std::vector<std::string> tokens;
    while (std::getline(ss, token, delimiter))
    {
        tokens.push_back(token);
    }
    return tokens;
}

TestingWindow::Backend TestingWindow::ParseBackend(const char* name,
                                                   BackendParams* params)
{
    *params = {};
    // Backends can come in the form <backendName>, or
    // <gpuNameFilter>/<backendName>.
    std::vector<std::string> tokens = split(name, '/');
    assert(!tokens.empty());
    params->gpuNameFilter =
        tokens.size() > 1 ? tokens[tokens.size() - 2].c_str() : "";
    const std::string nameStr = tokens.back();
    const auto nameStartsWith = [&](std::string str) {
        return nameStr.rfind(str, 0) == 0;
    };
    const auto nameEndsWith = [&](std::string str) {
        auto result = nameStr.rfind(str);
        return result != std::string::npos &&
               result == nameStr.size() - str.size();
    };
    if (nameStartsWith("angle"))
    {
        if (nameStartsWith("anglemsaa"))
            params->msaa = true;
        if (nameEndsWith("_mtl") || nameEndsWith("_metal"))
            params->angleRenderer = ANGLERenderer::metal;
        else if (nameEndsWith("_d3d") || nameEndsWith("_d3d11"))
            params->angleRenderer = ANGLERenderer::d3d11;
        else if (nameEndsWith("_vk") || nameEndsWith("_vulkan"))
            params->angleRenderer = ANGLERenderer::vk;
        else if (nameEndsWith("_gles"))
            params->angleRenderer = ANGLERenderer::gles;
        else if (nameEndsWith("_gl"))
            params->angleRenderer = ANGLERenderer::gl;
        return Backend::angle;
    }
    if (nameStr == "gl")
    {
        return Backend::gl;
    }
    if (nameStr == "glatomic")
    {
        params->atomic = true;
        return Backend::gl;
    }
    if (nameStr == "glcw")
    {
        params->clockwise = true;
        return Backend::gl;
    }
    if (nameStr == "glmsaa")
    {
        params->msaa = true;
        return Backend::gl;
    }
    if (nameStr == "d3d")
    {
        return Backend::d3d;
    }
    if (nameStr == "d3datomic")
    {
        params->atomic = true;
        return Backend::d3d;
    }
    if (nameStr == "d3d12")
    {
        return Backend::d3d12;
    }
    if (nameStr == "d3d12atomic")
    {
        params->atomic = true;
        return Backend::d3d12;
    }
    if (nameStr == "metal")
    {
        return Backend::metal;
    }
    if (nameStr == "metalcw")
    {
        params->clockwise = true;
        return Backend::metal;
    }
    if (nameStr == "metalatomic")
    {
        params->atomic = true;
        return Backend::metal;
    }
    if (nameStr == "vulkan" || nameStr == "vk")
    {
        return Backend::vk;
    }
    if (nameStr == "vulkanatomic" || nameStr == "vkatomic")
    {
        params->atomic = true;
        return Backend::vk;
    }
    if (nameStr == "vulkanmsaa" || nameStr == "vkmsaa")
    {
        params->msaa = true;
        return Backend::vk;
    }
    if (nameStr == "vulkancore" || nameStr == "vkcore")
    {
        params->core = true;
        return Backend::vk;
    }
    if (nameStr == "vulkanmsaacore" || nameStr == "vkmsaacore")
    {
        params->core = true;
        params->msaa = true;
        return Backend::vk;
    }
    if (nameStr == "vulkansrgb" || nameStr == "vksrgb")
    {
        params->srgb = true;
        return Backend::vk;
    }
    if (nameStr == "vkcw")
    {
        params->clockwise = true;
        return Backend::vk;
    }
    if (nameStr == "vkcwatomic" || nameStr == "vkcwa")
    {
        params->clockwise = true;
        params->atomic = true;
        return Backend::vk;
    }
    if (nameStr == "moltenvk" || nameStr == "mvk")
    {
        return Backend::moltenvk;
    }
    if (nameStr == "moltenvkcore" || nameStr == "mvkcore")
    {
        params->core = true;
        return Backend::moltenvk;
    }
    if (nameStr == "swiftshader" || nameStr == "sw")
    {
        return Backend::swiftshader;
    }
    if (nameStr == "swiftshadercore" || nameStr == "swcore")
    {
        params->core = true;
        return Backend::swiftshader;
    }
    if (nameStr == "dawn")
    {
        return Backend::dawn;
    }
    if (nameStr == "wgpu")
    {
        return Backend::wgpu;
    }
    if (nameStr == "wgpucw")
    {
        params->clockwise = true;
        return Backend::wgpu;
    }
    if (nameStr == "wgpumsaa")
    {
        params->msaa = true;
        return Backend::wgpu;
    }
    if (nameStr == "rhi")
    {
        return Backend::rhi;
    }
    if (nameStr == "coregraphics")
    {
        return Backend::coregraphics;
    }
    if (nameStr == "skia")
    {
        return Backend::skia;
    }
    if (nameStr == "null")
    {
        return Backend::null;
    }
    fprintf(stderr, "'%s': invalid TestingWindow::Backend\n", name);
    abort();
}

static void set_environment_variable(const char* name, const char* value)
{
#ifndef PLATFORM_NO_ENV_API
    if (const char* existingValue = getenv(name))
    {
        printf("warning: %s=%s already set. Overriding with %s=%s\n",
               name,
               existingValue,
               name,
               value);
    }
#ifdef _WIN32
    SetEnvironmentVariableA(name, value);
#else
    setenv(name, value, /*overwrite=*/true);
#endif
#endif
}

TestingWindow* TestingWindow::Init(Backend backend,
                                   const BackendParams& backendParams,
                                   Visibility visibility,
                                   void* platformWindow)
{
    assert((backend == Backend::rhi) == (s_TestingWindow != nullptr));

#if defined(_WIN32) && !defined(RIVE_UNREAL)
    // Set our backdoor GPU selection variables in case the API doesn't
    // allow us to select explicitly.
    const char* nameFilter = backendParams.gpuNameFilter.c_str();
    if (const char* gpuFromEnv = getenv("RIVE_GPU"); gpuFromEnv != nullptr)
    {
        // Override the program's GPU filter with one from the environment if
        // it's set.
        nameFilter = gpuFromEnv;
    }
    // "i" and "integrated" are special-case gpuNameFilters that mean "use
    // the integrated GPU".
    bool wantIntegratedGPU = static_cast<uint32_t>(
        strcmp(nameFilter, "integrated") == 0 || strcmp(nameFilter, "i") == 0);
    NvOptimusEnablement = !wantIntegratedGPU;
    AmdPowerXpressRequestHighPerformance = !wantIntegratedGPU;
#endif

    switch (backend)
    {
        case Backend::gl:
        case Backend::angle:
#ifndef RIVE_TOOLS_NO_GLFW
            if (backend != Backend::angle || visibility != Visibility::headless)
            {
                s_TestingWindow =
                    TestingWindow::MakeFiddleContext(backend,
                                                     backendParams,
                                                     visibility,
                                                     platformWindow);
            }
            else
#endif
            {
                s_TestingWindow =
                    MakeEGL(backend, backendParams, platformWindow);
            }
            break;
        case Backend::vk:
        case Backend::moltenvk:
        case Backend::swiftshader:
            if (backend == Backend::moltenvk)
            {
                // Use the MoltenVK built by
                // packages/runtime/renderer/make_moltenvk.sh
                constexpr static char kMoltenVKICD[] =
                    "../renderer/dependencies/MoltenVK/Package/Release/"
                    "MoltenVK/dynamic/dylib/macOS/MoltenVK_icd.json";
                set_environment_variable("VK_ICD_FILENAMES", kMoltenVKICD);
            }
            else if (backend == Backend::swiftshader)
            {
                // Use the swiftshader built by
                // packages/runtime/renderer/make_swiftshader.sh
                constexpr static char kSwiftShaderICD[] =
#ifdef __APPLE__
                    "../renderer/dependencies/SwiftShader/build/Darwin/"
                    "vk_swiftshader_icd.json";
#elif defined(_WIN32)
                    "../renderer/dependencies/SwiftShader/build/Windows/"
                    "vk_swiftshader_icd.json";
#else
                    "../renderer/dependencies/SwiftShader/build/Linux/"
                    "vk_swiftshader_icd.json";
#endif
                set_environment_variable("VK_ICD_FILENAMES", kSwiftShaderICD);
            }
#ifdef RIVE_ANDROID
            if (platformWindow != nullptr)
            {
                s_TestingWindow =
                    TestingWindow::MakeAndroidVulkan(backendParams,
                                                     platformWindow);
                break;
            }
#endif
            if (visibility == Visibility::headless)
            {
                s_TestingWindow =
                    TestingWindow::MakeVulkanTexture(backendParams);
            }
            else
            {
                s_TestingWindow =
                    TestingWindow::MakeFiddleContext(backend,
                                                     backendParams,
                                                     visibility,
                                                     platformWindow);
            }
            break;
        case Backend::metal:
#if defined(__APPLE__)
            if (visibility == Visibility::headless)
            {
                s_TestingWindow =
                    TestingWindow::MakeMetalTexture(backendParams);
                break;
            }
#endif
            s_TestingWindow = TestingWindow::MakeFiddleContext(backend,
                                                               backendParams,
                                                               visibility,
                                                               platformWindow);
            break;
        case Backend::d3d:
        case Backend::d3d12:
        case Backend::dawn:
            s_TestingWindow = TestingWindow::MakeFiddleContext(backend,
                                                               backendParams,
                                                               visibility,
                                                               platformWindow);
            break;
        case Backend::wgpu:
            s_TestingWindow = TestingWindow::MakeWGPU(backendParams);
            break;
        case Backend::rhi:
            break;
        case Backend::coregraphics:
            s_TestingWindow = MakeCoreGraphics();
            break;
        case Backend::skia:
            s_TestingWindow = MakeSkia();
            break;
        case Backend::null:
            s_TestingWindow = MakeNULL();
            break;
    }
    if (!s_TestingWindow)
    {
        fprintf(stderr,
                "Failed to create testing window for Backend::%s\n",
                BackendName(backend));
        abort();
    }

    return s_TestingWindow;
}

TestingWindow* TestingWindow::Get()
{
    assert(s_TestingWindow); // Call Init() first!
    return s_TestingWindow;
}

void TestingWindow::Set(TestingWindow* inWindow)
{
    assert(inWindow);
    s_TestingWindow = inWindow;
}

void TestingWindow::Destroy()
{
    assert(s_TestingWindow);
    delete s_TestingWindow;
    s_TestingWindow = nullptr;
}
